Loading MVC 4 default view with jQuery load() - asp.net

I have built an mvc 4 website, and I built it so that the main layout page doesn't refresh if a different section is loaded with jQuery. I put the navigator and jQuery script in _Layout.cshtml:
<ul id="menu" class="menu-items">
<li><a id="Item1" href="#" onclick="loadPage(this.id)">Item1</a></li>
<li><a id="Item2" href="#" onclick="loadPage(this.id)">Item2</a></li>
<li><a id="Item3" href="#" onclick="loadPage(this.id)">Item3</a></li>
<li><a id="Item4" href="#" onclick="loadPage(this.id)">Item4</a></li>
</ul>
</body>
<script>
function loadPage(action) {
$.post("/Home/" + action, function (data) {
$(content).html(data);
});
}
</script>
Then I have my controller:
namespace MyApp.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Item1()
{
if (Request.IsAjaxRequest())
{
return PartialView();
}
return View();
}
[HttpPost]
public ActionResult Item2()
{
if (Request.IsAjaxRequest())
{
return PartialView();
}
return View();
}
Etc, etc.
Everything works fine, except that I don't know how to use just one main content view (which is index.cshtml when the website loads in the browser). I'm forced to put the same content that's in index.cshtml into item1.cshtml so that when I trigger onlick for item1, it will go back to the main content. The only route config I have is for the Default, which initially set to Index:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
What I want, is to be able to use just one main content page, but have the ajax call still get me back to the main content when I click Item1. Does anyone know what I need to do? It seems to be a little overkill to have to update both views when I want to update the main content.
Also, I think other web devs will like this code. Especially if you're building a band's website like I'm doing. It allows me to put the demo song media player in the _layout.cshtml page so that it won't refresh when the user is clicking to the other sections (i.e. if it refreshes, the media player stops). With this design, the user can navigate the whole website while the songs continue to play.
I'm rather new to javascript, so I'm sure I could have made a better onclick handler rather than using anchor tags, so if anyone want to show me a better way, please do. But my main problem is the index.cshtml vs item1.cshtml dilemma.

Correct me if I'm wrong: you want to refresh part of your page when clicking on ItemX link and the controller methods ItemX are only used via Ajax (as you're building a single page app).
In this case you could do something like this:
Cshtml
<ul id="menu" class="menu-items">
<li><a id="Item1" href="#" onclick="loadPage(this.id)">Item1</a></li>
<li><a id="Item2" href="#" onclick="loadPage(this.id)">Item2</a></li>
<li><a id="Item3" href="#" onclick="loadPage(this.id)">Item3</a></li>
<li><a id="Item4" href="#" onclick="loadPage(this.id)">Item4</a></li>
</ul>
<div id="container">
</div>
</body>
<script>
function loadPage(action) {
$.post("/Home/" + action, function (data) {
$("#container").html(data);
});
}
// Will load Item1 via Ajax on page load
loadPage('Item1');
</script>
Home Controller
[HttpPost]
public ActionResult Item1()
{
return PartialView();
}
Your PartialViews should only contain the HTML specific to the current item.
Update
If you wish to avoid the Ajax call you could do this also in your cshml
...
</ul>
<div id="container">
#Html.Partial("Item1PartialView")
</div>
</body>
...

Related

ASP.Net Core MVC - Validation Summary not working with bootstrap tabs and dynamically loaded content

How do you get dynamically loaded tabs to work in ASP.Net Core MVC?
I have a simple Index.cshtml that uses bootstrap tabs to create two tabs from the a tags on the page. (To test out options, I first copied from https://qawithexperts.com/article/asp.net/bootstrap-tabs-with-dynamic-content-loading-in-aspnet-mvc/176)
There is a click event on each tab that uses $.ajax() to call the controller and then set the html of the appropriate div.
I have a model with one field, a string that is required.
I have the create view that Visual Studio created.
When I run it and click the first tab, the controller returns PartialView("FirstTabCreate") and loads into the div and everything looks great.
The problem is when clicking the "Create" button.
The controller method checks if IsValid on the ModelState. If not, here is where I run into a problem. If I return the partial view and the model that was passed in I see my validation errors as expected but because I returned the partial view, I lose my tabs. If I return the main view (Index) then the javascript reloads my partial view and has lost the ModelState at that point.
I am not sure what to return so that this works. I have seen lots of examples online that use dynamically loaded tabs but none of them have models or validation.
Code below:
Index Page
#model FirstTab
<!-- Tab Buttons -->
<ul id="tabstrip" class="nav nav-tabs" role="tablist">
<li class="active">
Submission
</li>
<li>
Search
</li>
</ul>
<!-- Tab Content Containers -->
<div class="tab-content">
<div class="tab-pane active" id="FirstTab">
</div>
<div class="tab-pane fade" id="SecondTab">
</div>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script>
$('#tabstrip a').click(function (e) {
e.preventDefault();
var tabID = $(this).attr("href").substr(1);
$(".tab-pane").each(function () {
console.log("clearing " + $(this).attr("id") + " tab");
$(this).empty();
});
$.ajax({
url: "/#ViewContext.RouteData.Values["controller"]/" + tabID,
cache: false,
type: "get",
dataType: "html",
success: function (result) {
$("#" + tabID).html(result);
}
});
$(this).tab('show');
});
$(document).ready(function () {
$('#tabstrip a')[0].click();
});
</script>
FirstTabCreate View
#model WebApplication1.Models.FirstTab
<h4>FirstTab</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="FirstTabCreate">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
Model
using System.ComponentModel.DataAnnotations;
namespace WebApplication1.Models
{
public class FirstTab
{
[Required()]
public string FirstName { get; set; }
}
}
Controller
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using WebApplication1.Models;
namespace WebApplication1.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public ActionResult FirstTab()
{
return PartialView("FirstTabCreate");
}
public ActionResult FirstTabCreate(FirstTab model)
{
if (!ModelState.IsValid)
{
return View("FirstTabCreate", model);
}
return Content("Success");
}
public ActionResult SecondTab()
{
return PartialView("_SecondTab");
}
}
}
I don't like it but to get it to work, when I click Save, in the Controller method I check if the ModelState is valid. If not, I put the keys and values into a list of custom class and then put that list in the cache. When the child partial view loads it checks to see if there is anything in the cache and if so, parses it back out and uses ModelState.AddModelError().
It's not pretty but it does allow the validation to work.
try to add jquery validation scripts in your code
delete this
<script src="~/lib/jquery/dist/jquery.min.js"></script>
and use this instead
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Add below code to your #section Scripts
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
<script>
$.validator.setDefaults({
ignore: []
});
</script>
}
Note: do not add the above inside $(document).ready()

Calling an action with parameter .Net Core

I have a MyMessages model that I put data into when the user logs in. it includes all the data needed.
for every message that they have I do the following in view
for (int i = 0; i < Model.MyMessagesCount; i++)
{
<li class="list-group-item d-flex align-items-center mt-2">
<a>#Model.MessagesTitles[i]</a>
</li>
When the user clicks on each of the <a>, I want to show them that message in more details in a separate view where I pass MessageID to.
How can I achieve that? How can I have all the <a> call the same action but with different MessageID as a parameter? (Something like this /User/Usermessages?MessageID=20)
You can use anchor tag helper
<li>
<a asp-controller="user"
asp-action="messages"
asp-area=""
asp-route-messageId="#Model.MessagesTitles[i].MessageId">
#Model.MessagesTitles[i]
</a>
</li>
asp-route-{parameter}: the parameter there is the name of the parameter you define in your action.
You can read more on https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-5.0
In the Index.cshtml page, you could directly set the a tag href attribute.
Code sample as below:
public IActionResult Index()
{
List<MyMessages> messages = new List<MyMessages>()
{
new MyMessages(){ MessageId=1001, MessageTitile="AA"},
new MyMessages(){MessageId =1002, MessageTitile="BB"},
new MyMessages(){MessageId=1003, MessageTitile="CC"}
};
return View(messages);
}
public IActionResult Details(int messageId)
{
return View();
}
Index.cshtml page:
#model List<SignalRChatSample.Models.MyMessages>
<ul>
#for (int i = 0; i < Model.Count(); i++)
{
<li class="list-group-item d-flex align-items-center mt-2">
#Model[i].MessageTitile
</li>
}
</ul>
Then, the result like this (The url: Https://localhost:5001/Home/Details?messageId=1002):
Besides, as David said, you could also send the parameters via the route.

Controller action not being called on button click

I have an ASP .Net core MVC 2.0 app where I implemented a shopping cart for an e-commerce site as outlined in this video series. In the app, user can enter search queries for items which are displayed in a table. Each item in the table can be clicked on to display another page that has additional details for that item as well as a add to cart button.
The site features for searching, displaying the item results, and the details page are all implemented as actions in one controller class while the shopping cart functionality is all implemented in a separate controller class.
For some reason, when I click on the "Add to Cart" button for an item, a url of the form http://<hostname>/<controllerName>/<controllerACtion>/<id>is requested, but the corresponding controller action isn't called. This is strange because I'm using essentially the same code to call the action for adding to shopping cart as I did for displaying details. For this reason, I think the issue is related to the setup of the routes.
Here is the route setup in startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=resortDeals}/{action=Index}/{id?}");
routes.MapRoute(
name: "shoppingcart",
template: "{controller=ShoppingCartController}/{action=AddToShopingCart}/{id?}");
});
Here is the shopping cart controller structure:
public class ShoppingCartController : Controller
{
public ViewResult Index()
{
...
}
public RedirectToActionResult AddToShopingCart(int dealId)
{
...
}
}
Below is the code for how the action is being called once the add to cart button is clicked:
<div class="addToCart text-right">
<p class="button ">
<a class="btn btn-success" id="cartButton" asp-controller="ShoppingCartController" asp-action="AddToShoppingCart" asp-route-id="#Model.RecNo">
Add to cart
</a>
</p>
</div>
What could be preventing the controller action from being called once the button is clicked?
I had an issue after publishing on the production server, when clicking on the button there was no response and I had to submit the button using Javascript.
<div class="addToCart text-right">
<p class="button ">
<button class="btn btn-success" onclick="search(#Model.RecNo)">
Add to cart
</button>
</p>
</div>
<script>
function addToCart(recNo) {
document.getElementById('cartForm').action = "/ShoppingCart/AddToShoppingCart/" + recNo;
document.getElementById('cartForm').submit();
}
</script>
I don't think you need the following route because the default route will handle the shoppingcart routes as well.
template: "{controller=ShoppingCartController}/{action=AddToShopingCart}/{id?}");
Also, try
<a class="btn btn-success" id="cartButton" asp-controller="ShoppingCart" asp-action="AddToShoppingCart" asp-route-id="#Model.RecNo">
I have removed the Controller word from asp-controller attribute
Also, change the input parameter name of AddToShopingCart method to id instead of dealId
public RedirectToActionResult AddToShopingCart(int id)
{
...
}

Create dynamic links in the navigation panel

I am trying to dynamically create my top navigation panel in ASP.NET Core MVC 6.
I know it's simple but I cannot figure out how to make it work. Here is what I do (simplified):
My Model:
public class IP_Category
{
public int id { get; set; }
public string DisplayName { get; set; }
}
in my controller:
public IActionResult Index()
{
//This way I dynamically pass data to my View
ViewBag.Categories = _repository.ReturnCategories();
return View();
}
in my cshtml page:
#{
//this info is in the top of the page, here I retrieve data passed from
//controller and save it as a local variable
var categories = (List<IP_Category>)ViewBag.Categories;
}
Then later in the _Layout where I take care of the navigation:
<ul class="nav navbar-nav">
<li><a asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-controller="Home" asp-action="Contact">Contact</a></li>
#foreach (var category in categories)
{
<li><a asp-controller="Home" asp-action="#category.DisplayName">#category.DisplayName</a></li>
}
</ul>
The problem occurs with asp-action="#category.DisplayName" which does not generate appropriate href in my actual page.
So the question is what am I doing wrong? How can I pass category.DisplayName to my asp-action tag so the links work correctly?
Edit 1 - Adding more code:
Here is what was generated (note the missing href tag)
<ul class="nav navbar-nav">
<li>Home</li>
<li>About</li>
<li>Contact</li>
<li>Item1</li>
<li>Item2</li>
</ul>
if the controller and action don't match an existing controller and action then no href will be rendered. I suspect that #category.DisplayName does not match any actual action name on your home controller. Seems more likely that you have an action named Category that expects a parameter corresponding to the #category.DisplayName so it should be passed as a route parameter not as the action name
#foreach (var category in categories)
{
<li><a asp-controller="Home"
asp-action="Category"
asp-route-category="#category.DisplayName">#category.DisplayName</a></li>
}

How to return Nested PartialViews (including their javascript) from an AJAX call in ASP.Net MVC

I have created a treeview of Categories using nested partial views:
my Index page (that displays the treeview):
<div>
Category Menu:
<input type="button" value="1" name='selectCat_btn' />
<input type="button" value="2" name='selectCat_btn' />
</div>
<!-- Treeview -->
<% Html.RenderPartial("ItemCats_UL", Model); %>
<div id="CatSelectorOutput">
</div>
ItemCats_UL:
<div>
<ul id="catsTree">
<% Html.RenderPartial("ItemCats_LI", Model); %>
</ul>
</div>
<script type="text/javascript" >
$(document).ready(function() {
$("#catsTree").treeview();
</script>
ItemCats_LI:
<%foreach (ItemCategory itemCat in Model)
{ %>
<li>
<%= itemCat.Name %>
<%if (itemCat.Children != null && itemCat.Children.Count() > 0)
{ %>
<ul>
<% Html.RenderPartial("ItemCats_LI", itemCat.Children); %>
</ul>
<%} %>
</li>
<%} %>
Now this treeview works perfectly when I return the basic View("Index", Model) from my controllers Index action on page load.
The trouble comes when I want to change the Categories Model displayed in my Treeview (the nested partialViews) from an AJAX call...
For example: I click one the 'Cats2' button and the page should display Categories with ParentID of 2 in the Treeview. I attempted this by returning a JsonResult of the html of the ItemCats_UL PartialView (using a RenderPartialToString method found here) from my Controller Action. As some of you might know Javascript won't run in your partial view when you use an AJAX form to return a PartialViewResult, and I need Javascript in my Treeview which is why I'm using the RenderPartialToString.
The category select button click handler:
<script type="text/javascript">
$("[name='selectCat_btn']").click(function() {
var CID = $(this).attr('value');
$.ajax({
type: "POST",
url: "SelectCat",
dataType: "json",
data: { "CID": CID },
success: function(result) { $("#CatSelectorOutput").html(result.output); }
});
return false;
});
</script>
My Controller Action:
[AcceptVerbs(HttpVerbs.Post)]
[UrlRoute(Name = "SelectCat", Path = "selectCat")]
public ActionResult SelectCat(int CID)
{
IQueryable<ItemCategory> cats;
cats = ItemRepo.GetItemCats().WithCID(CID);
JsonResult result = null;
result = new JsonResult
{
Data = new
{
success = true,
output =
Helpers.RenderHelper
.RenderPartialToString("~/Views/Admin/AdminItemCatsUL.ascx",
cats)
}
};
return result;
}
The result:
The ItemCats_UL partialView displays! BUT the nested PartialViews (ItemCats_LI) don't!
Error I receive when I step through the markup in the ItemCats_UL.ascx and hover over the 'Html' part of the following code:
<ul id="catsTree">
<% Html.RenderPartial("ItemCats_LI", Model); %>
</ul>
Value cannot be null.
Parameter name: viewContext
Html = 'Html' threw an exception of type 'System.ArgumentNullException'
I'm wondering if there's a clever guy out there who can extend the RenderPartialToString method to include nested partialviews? Or am I missing something simple?
You need to hook the newly returned HTML / JavaScript back into the DOM upon loading it.
I'm sure there are lots of ways to do this, but I found a nice jQuery add-on called LiveQuery (link)
that helps me do it.
To make it work in your case, you'd set up a jQuery document.ready function in the parent page that looks something like this:
$("#catsTree").livequery(function () { this.treeview(); }, function () { /* code to destroy the treeview here */ });

Resources