How to create consistent URLs with MVC .NET routing? - javascript

I am running ASP.Net MVC 5 in .NET Framework 4.8. I keep getting 404 error due to inconsistently generated URLs. For example, in my _PageNav.chstml partial that is included at the top of each page I the following to take the user back to the home page: #Url.Action("Index", new { controller = "Home" }). In the navigation bar this resolves to <a class="navbar-brand" href="/" /> and functions properly.
When I use the same #Url.Action("Index", new { controller = "Home" }) on the same page, but later in on a button, it resolves to this: <a id="doneButton" class="btn btn-secondary px-4" href="https://localhost:44337/smrt/">Done</a>
Because of this inconsistency I often have issues where AJAX JavaScript references to the controllers end up with missing controller references such as /create resulting in https://localhost:44337/create instead of https://localhost:44337/home/create or /home/create resulting in https://localhost:44337/home/home/create instead of https://localhost:44337/home/create
I do have also have some limitations because of security restrictions; for example I cannot have any JavaScript on the page itself so I can't write razor code in my .cshtml files that will result in JavaScript. I can only use JavaScript referenced in source files for the page.

#Url.Action("Index", "Home") should be enough to do the job but I've noticed some issues sometimes where custom routing is at play, when changing levels. The Url.Action routine does not render fully qualified urls and sometimes I found I had to append "../" to navigate to a different controller structure, when one controller's view makes an AJAX call to another folder's view. Also, the default URL structure can throw off relative URL calls because the default implementation is to hide /Index in the URL structure. So the URL:
localhost/site (defaults Home/Index)
localhost/site/other (defaults the Index)
Sees the two views in a different folder structure, and URL navigation can get thrown off.
That has been my experiences as to why you may see some of the problems you are seeing.

It took a few different ideas, but I was able to solve my problem. I now get consistent URLs in just they way I need them, every time.
First, I used this article How to Build Absolute Action URLs Using the UrlHelper Class, to create the follwoing class with an extension method based on the UrlHelper Class .
using System.Web.Mvc;
namespace MVCPages.Utilities
{
public static class UrlHelperExtenions
{
/// <summary>
/// Generates a fully qualified URL to an action method by using
/// the specified action name, controller name and route values.
/// </summary>
/// <param name="url">The URL helper.</param>
/// <param name="actionName">The name of the action method.</param>
/// <param name="controllerName">The name of the controller.</param>
/// <param name="routeValues">The route values.</param>
/// <returns>The absolute URL.</returns>
public static string AbsoluteAction(
this UrlHelper url,
string actionName,
string controllerName,
object routeValues = null
)
{
var httpContext = url.RequestContext.HttpContext;
string scheme = httpContext.Request.Url.Scheme;
return url.Action(
actionName,
controllerName,
routeValues,
scheme
);
}
}
}
This new method dynamically gives me the root of the website no matter where the site is deployed.
I then call the new method extension in the views where I need URL consistency and pass the absolute root URL to the view using the ViewBag.
public ActionResult Index()
{
string rootUrl = Url.AbsoluteAction("Index", "Smrt");
ViewBag.RootUrl = rootUrl;
}
Since I am not allowed to use Razor to create JavaScript, I next use Razor to create a hidden HTML div tag that includes the root URL, <div id="rootUrl">https://rooturl.com/</div>
Now, in JavaScript, I use the DOM to retrieve the URL from the HTML tag; then use ad that to my ajax calls to create a complete URL for the AJAX calls.
window.onload = function () {
var rootUrl = document.getElementById("rootUrl").innerHTML;
var ajaxUrl = rootUrl + ajaxCall;
};

Related

How to get the url that called the controller in a Spring thymeleaf application

I've got two pages that link to the same new page. What I need to be able to do is pass the current page so that the new page can return to it after it has done what needs to be done.
Suppose I have two pages:
/user/46#updateInfo and
/child/993#updateInfo
Both pages have a button
<a id="upload-image" th:href="#{/documents/upload/__${id}__}" class="btn btn-primary"> Upload Image </a>
which triggers the controller:
#GetMapping({documentType}/{userId})
public ModelAndView uploadImage(ModelAndView mav,
#PathVariable(name="documentType") Optional<DocumentType> documentType,
#PathVariable(name="userId") Optional<String> userId,
#RequestParam(name="returnUrl") Optional<String> returnUrl) {
// do stuff here
mav.addObject("returnUrl", returnUrl);
mav.setViewName("uploadImages");
return mav;
}
I've tried adding in the script section of the pages with the upload button the following:
$(document).ready(function() {
$('#upload-image').prop("value", window.location.pathname + window.location.hash);
$('#upload-image').prop("returnUrl", window.location.pathname + window.location.hash);
}
But that value doesn't show up in the controller. And I'm thinking there has to be an easier way.
Again, the goal is when the new page is loaded (in this example uploadImages page), the user will either upload an image or click the Cancel button and then be returned to either the /user/46#updateInfo or
/child/993#updateInfo page, which ever page initially called the uploadImages page.
Add HttpServletRequest object to the list of arguments of your controller method and call getRequestURI() method on that object within that controller.That should do the job - the method returns a String containg a full path of requested URI

reference javascript content coming from db instead of static file

I am developping an asp.net web application :
In the website folder, I have a folder called "integrations" containing a list of js files (name1.js, name2.js, name3.js ...)
A user makes a http request to a mvc controller method where he gives as input a "name".
This method gives the "name" as a property of a viewmodel to a razor view containing the following code :
This razor view is returned to the user.
The previous code is working well, but I would like to add an improvment. Actually you need to add a js file inside the folder integrations,
and you cannot do that when the application is running in production.
So I would like instead of having a script tag referencing a file placed inside the integrations folder to have a script tag containing a js content
coming from a data table like this :
name : jscontent
name1 : jscontent1
name2 : jscontent2
But I don't know how to changed this :
<script src="~/integrations/#(Model.Name).js"></script>
To :
<script>>query in db by #(Model.Name) parameter to get corresponding jscontent</script>
Make it a controller/method and you can take advantage of things like caching, auth etc for free:
public class ScriptController
{
public ActionResult Content(MyModel model)
{
var result = "alert("Hello World!");";
return JavaScript(result);
}
}
JavaScriptResult
Then in your html:
<script src="~/Script/Content?prop1=whatever"></script>

ASP.NET MVC / JQuery/ Javascript: view will not refresh after searching (and trying to load) new data

This is my first post of stackoverflow. I've spent the past month trying to solve this on my own, extensively searching Google and this website. Here's my problem:
I have a website where users can search for cases. When they find a case, their results are loaded on a case details page. Users normally search for cases from the homepage by clicking a search option, where they enter a case number like the following:
14-12345
Users submit their search to the homepage's index controller. The controller parses the search and redirects to a "case" action. This action polls the database to get case details, and returns a case view.
This search works - users see the results of their search on a case details page. However, a request was received so users can search for cases from the case details page as well.
I can't make the new request work. I've tried using Web API (which really became a waste of time, because I want to return a whole view, not just search data), and I've failed to create the appropriate controller/view combination to work with my data. I usually wind up trying to use the existing controller (which has the code to search) and the case details view.
Breaking down the pieces...
The model data is stored in a viewmodel file:
public class PortalCaseView
{
public DocketCase CaseInfo { get; set; }
public List<CaseNote> Notes { get; set; }
public string Search { get; set; }
...other various variable declarations, etc
}
The Index.cshtml file is the homepage/main landing page for the site. Users can search for case details by going to a section to search (code from the view here):
<div class="tile">
<span>Search by Case Number</span>
#Html.EditorFor(x => x.Search)
<a class="m-btn green-stripe" href="javascript:submitForm();" style="color: #444;
text-decoration: none;">Submit<i class="icon-hdd"></i></a> <a class="m-btn red-stripe"
href="javascript:toggleSearch();" style="color: #444; text-decoration: none;">Cancel<i
class="icon-remove"></i></a>
</div>
(Submitting the result gives a submit command, which posts the search to the controller.)
The PortalController.cs controller file directs requests for the homepage. Its Index method grabs the Search variable, and redirects to a Case action to process:
Index
if (!string.IsNullOrWhiteSpace(viewmodel.Search))
{
...
return RedirectToAction("Case", new { Year = docketnumber[0], Sequence = docketnumber[1], J = viewmodel.JudgeCode });
}
Case
[HttpGet]
public ActionResult Case(int Year, int Sequence, string J)
{
...various declarations and requests to get db information...
return View(vm); //vm is a viewmodel with info for the case view
}
[HttpPost]
public ActionResult Case(PortalCaseView vm)
{
return View(vm);
}
* When the redirect to the Case action is complete, the Case.cshtml view loads with the necessary details. Now that searches from the case view are required, I've added a section to the case view to take an "on-demand" search query:
<textarea id="searchForCase" style="width: 150px;"></textarea>
<a class="m-btn green-stripe" href="javascript:searchCase();" style="color: #444;
text-decoration: none;">Search<i class="icon-hdd"></i></a>
And here is where problems start. During a typical run, Ajax/JSON code builds a call back to a controller action. Then, the action executes (usually on data stored in a file or folder). A view is returned, and then the page refreshes. (This is when a user is using the site.) It doesn't work the same way for my new custom code.
My custom searchCase() function takes the case number entered by a user to search for it (it goes back to the Index action in the original PortalController.cs file):
var searchCase = function () {
var textArea = document.getElementById("searchForCase");
var txt = String(textArea.value);
jQuery.ajax({
type: "POST",
url: "#Url.Action("Index","Portal")",
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ fromCaseSearch: txt }),
success: function (data) {
alert(data);
},
failure: function (errMsg) {
alert(errMsg);
}
});
$('form').submit();
}
(Note: I've already tried changing the datatype from json to html; I'm assuming I might need to work with the content type)
I've traced the progress from this point. The website goes back to PortalController.cs and to the Index action (following through like a good POST), and then takes the search query provided by the user. Since it falls out of the Index action when I try to redirect to the Case action like before, I added an if statement to deal with search queries directly from the case details view:
if (!string.IsNullOrEmpty(fromCaseSearch))
{
System.Web.HttpContext.Current.Session.Clear();
//forget why I put that clear request there, but I do
//use session state variables, so I might need to clear
//them when searching from scratch - if they exist
viewmodel = new PortalIndexView();
viewmodel.Search = fromCaseSearch;
...initialization and parsing for search...
...searching for and loading data from database(s)...
ModelState.Clear(); //was hoping this would "refresh" view
//...this didn't work either
return View("Case", vm); //forced website to return
//case view with viewmodel data
//...this actually starts loading
//the case view
}
This actually works... to a point. The data gets processed the same way, and the case view is loaded like normal. However, even after tracing the loading process (and seeing that model variables are sent to the view), the page does not update with the new information).
So, that's where I'm stuck. I've tried tweaking some settings (and even thought the problem might be in the web.config file), but when I run across problems like this, what usually fixes the problem for me is to find out what I did wrong here (or in the general vicinity of the problem - the answers usually happen when I fix simple stuff first).
Some last-minute things:
PortalController.cs (the controller) outputs to Case.cshtml (the case details view). When right-clicking the View controller action, it redirects back to PortalController.cs (the main controller from the landing page).
No other controller shares Case.cshtml.
There is no CaseController.cs file. It looks like my predecessor simply created the search "redirect", figuring users would only search from the homepage (and they did up until now).
Last-second idea? Maybe I'm supposed to account for HTTPGet and HTTPPost actions for my new code as well. Not sure... brain is mush...
Naturally, since this is a work project, I can only provide so many details, but I'll be glad to take suggestions at this point.
UPDATE: is it possible that my problem is because I didn't include a #using (Html.BeginForm(...)) line in my case details view? I noticed there isn't one. Do you need one to be able to have official POST action in the related controller?
One other clarification: I'm using Ajax/JSON for passing data back to the controller from the case view, since I can't get the page to just "submit" (in Javascript/JQuery code, $('form').submit() does not work at all). I'm wondering if it has anything to do with Web API. (There is a Web API file called CaseNoteController.cs that handles notes that get added to cases. When I was trying to work with Web API - and possibly return a view using it - I had a test api call that made the case details view page refresh effortlessly; I just couldn't figure out how to get it to work for my needs.)
Problem solved (my fault). My lack of experience kept me from figuring this out sooner:
I wound up not needing the searchCase function in the Case view. The reason why: I didn't wrap my Case view in a #using(Html.BeginForm()) code block, which meant that my page wasn't posting results (no matter how hard I tried)
I didn't pay attention to my PortalController.cs file, which already had HttpGet and HttpPost variants of my Case action. (The HttpPost action was completely empty, save for returning a view. That explains why the page failed to load and display anything, let alone the results of my next case search.)
Once I corrected those issues (and tweaked a few other buttons so they didn't automatically post or try to run when I submitted the document), the code finally worked! Praise God. No need for convoluted or weird paths back to the controller - things happened real quick after that.
(Now to trim off a whole bunch of scaffolding and otherwise unnecessary code...)

Is there any way to use the JQuery GetJSON method to get HTML from an external page?

So let's say you're trying to do a jquery ajax request, something like:
$.ajax({
...
url: http://other-website.com
...
})
I understand that because of the same-origin principle, this request will fail because the URL is an external domain.
However I've heard that GetJSON() does not obey this principle and can send asynchronous get requests to external servers using JSONP and an appended URL.
My question is: is it possible to use GetJSON() to retrieve all the HTML from an external name as a single string within a JSON object? If it doesn't do so by default, is there any way I can force / trick it into doing so?
Yes, you can request html from a remote location, however you must use a proxy to do so. One publicly available proxy is YQL.
http://jsfiddle.net/BKJWu/
var query = 'SELECT * FROM html WHERE url="http://mattgemmell.com/2008/12/08/what-have-you-tried/" and xpath="//h1" and class="entry-title"';
var url = "http://query.yahooapis.com/v1/public/yql?q=" + query + "&format=json&callback=??";
$.getJSON(url,function(data){
alert(data.query.results.h1.content);
})
You could of course build your own on your server that returns plain html rather than json.
The answer is no, you cannot trick it or force it to load html from an external source. GetJSON only works on servers that serve JSONP, and only valid JSON objects are able to be read.
You can retrieve any JSON object that you have access to with GetJSON. Here is an example with Razor an MVC Controller.
jQuery Code
$(function () {
$.getJSON('#Url.Action("GetColorsJson", "Json")', function (jsonData) {
var css = new customContentJs.css.apply(jsonData);
});
});
Controller Code
using System.Web.Mvc;
using DAL;
using Newtonsoft.Json;
public class JsonController : Controller
{
private readonly CustomContentContext _db = new CustomContentContext();
/// <summary>
/// Return a json serialized object of user saved colors
/// </summary>
/// <returns></returns>
public string GetColorsJson()
{
return JsonConvert.SerializeObject(_db.Site.Include("Colors"));
}
}

Is there an ASP.Net MVC view engine that supports JavaScript views?

I would like to generate some JavaScript on the server side in ASP.Net MVC. Is there a view engine that supports this? Ideally I would like to be able to get JavaScript from an url like:
http://myapp/controller/action.js
I've looked at the MonoRail project, and they seem to have this feature, but it's very lacking in documentation, and I can't find any ports to ASP.Net MVC.
Edit: The idea is to be able to render a page both as standard HTML by using a url like:
http://myapp/controller/action
and as js (specifically an ExtJS component) by using the first url in the question. There would be only a single action in the controller, but two views: one for HTML and one for JS.
Edit 2: I basically wanted to achieve the same result as router extension parsing/request handling in CakePHP.
I wanted to extend this idea to not only allow Javascript views, but more or less any type of document. To use it, you just put the views for *.js urls in a subfolder of your controller's view folder:
\Views
+-\MyController
+-\js
| +-Index.aspx <- This view will get rendered if you request /MyController/Index.js
+-Index.aspx
The class is a decorator for any type of ViewEngine, so you can use it with NVelocity/WebForms/Whatever:
public class TypeViewEngine<T> : IViewEngine where T : IViewEngine
{
private readonly T baseEngine;
public T BaseEngine
{
get { return baseEngine; }
}
public TypeViewEngine(T baseEngine)
{
this.baseEngine = baseEngine;
}
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"TypeViewEngine",
"{controller}/{action}.{type}",
new {controller = "Home", action = "Index", type = "html"}
);
}
public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
var vars = controllerContext.RouteData.Values;
if(vars["type"] != null && vars["type"].ToString() != "html")
{
viewName = string.Format("{0}/{1}", vars["type"], viewName);
}
return baseEngine.FindView(controllerContext, viewName, masterName);
}
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName)
{
return baseEngine.FindPartialView(controllerContext, partialViewName);
}
public void ReleaseView(ControllerContext controllerContext, IView view)
{
baseEngine.ReleaseView(controllerContext, view);
}
}
Then, in your Global.asax.cs file:
protected void Application_Start()
{
var ve = new TypeViewEngine<WebFormViewEngine>(new WebFormViewEngine());
ve.RegisterRoutes(RouteTable.Routes);
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(ve);
}
Thanks for everyone's help with this!
Based on your edit I'll try with a new answer asumming you need json data for ExtJS. I've just tested it in the app I'm building and it works fine. First you need two routes
{controller}/{action}.{format}
{controller}/{action}
Now the Controller class has a Json method to serialize whatever object you want and it's a JsonResult so you can just use:
public ActionResult List(string format) {
// logic here
if (string.IsNullOrEmpty(format)) {
return View();
} else if (format == "js") {
return Json(object_to_serialize);
}
}
The July 2007 ASP.NET Futures release has the new Managed JScript (I can't find a newer one). I've successfully batted it around a bit - but beware, you will probably be forced to do some extra work, the .ASPX parsing for JScript is unusably buggy.
http://www.microsoft.com/downloads/details.aspx?FamilyId=A5189BCB-EF81-4C12-9733-E294D13A58E6&displaylang=en
You don't necessarily need view engine for that, just return plain text result.
Here is a controller (MonoRail) I've used to render some user culture settings in a javascript object:
[ControllerDetails("js")]
public class JavascriptController : Controller
{
private ISessionContext sessionContext;
public JavascriptController(ISessionContext sessionContext)
{
this.sessionContext = sessionContext;
}
public void CultureInfo()
{
var scriptformat = #"var cultureInfo = {0};";
var json = Context.Services.JSONSerializer.Serialize(getJSONiZableCultureInfo(sessionContext.CurrentCulture));
RenderText(String.Format(scriptformat, json));
}
object getJSONiZableCultureInfo(System.Globalization.CultureInfo culture)
{
return new
{ // add more there
culture.NumberFormat
};
}
}
for more complex things for which raw text rendering would smell, any view engine would work.
Also you are not forced to use .js extension to put the url in your script tag.
I had to do something very similar, and I used the nvelocity view engine from MVCContrib - technically you could use the default aspx view engine, but I found that the nvelocity syntax was a lot more straightforward for puting out script (don't worry if you haven't used it before - I spent about 10 minutes figuring it out!).
Then you just need to add a route to the route table to handle directing your .js url to the action!
EDIT
Can't verify this as I don't have visual studio to hand but for the route, you might have something like this:
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action].js",
Defaults = new { controller="home", requestType="javascript" }, // Whatever...
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]",
Defaults = new { controller="home"}, // Whatever...
RouteHandler = typeof(MvcRouteHandler)
});
Requests ending in .js should go through the first route - extensionless requests fall through to the second.
Then your action could have a requestType param:
public ActionResult MyAction (RequestType requestType)
{
if(requestType == RequestType.JavaScript)
{
... new nvelocity view to render javascript
}
else
{
...
}
}
As for directory structure - you're on your own with that! Not because I don't want to be helpful, but more down to the fact that you have flexibility to do what you want with it!
If you just want to generate a javascript based on the ViewData you can create your own custom result. Here is an example.

Categories