I have an asp.net mvc site to which I've added some knockoutjs. The knockout code makes ajax request for data from the controllers e.g.
$.getJSON(BASE_URL + 'MyTasks/GetDataPage', { userKey: vm.UserKey, pageSize: pageSize }, function (returnedPayload) {
data = returnedPayload.filter(function (item) {
return JSON.stringify(item).toLowerCase().indexOf(ft) != -1;
});
self.setPagingData(data,page,pageSize);
The BASE_URL constant I set in the <head> of my layout razor page as follows:
<script type="text/javascript">
var BASE_URL = '/bamportal/';
</script>
All works fine when the website is deployed. However, when I run the website from VS by hitting F5 then I get a 404 such as:
http://localhost:49601/bamportal/MyTasks/GetDataPage?userKey=2&pageSize=50 Failed to load resource
If it had tried to address "http://localhost:49601/MyTasks/GetDataPage" (without the "/bamportal/") it would work.
What's the best solution for this problem?
Quick and dirty:
<script type="text/javascript">
var BASE_URL = '#Constants.BaseUrl';
</script>
where Constants is a static class defined as:
public static class Constants {
#if DEBUG
public const string BaseUrl = "/";
#else
public const string BaseUrl = "/bamportal/";
#endif
}
So when you compile your application in debug configuration you will get /, while in release you will get /bamportal/.
As alternative, a more complex and versatile approach could be obtained using Configuration Transforms and appSettings from Web.config:
<script type="text/javascript">
var BASE_URL = '#System.Configuration.ConfigurationManager.AppSettings["BaseUrl"];';
</script>
This, of course, will be extremely useful in scenarios where you need to deploy front-end and back-end on different domains/urls.
Your code should never know where the site will be hosted, you should use the correct helpers to determine where the action/content is located, this will prevent any issues with paths. Use Url.Content and Url.Action they will generate the correct path/url in the code.
As an example your action needs to point to the "MyTasks", "GetDataPage"
In your razor code you should have something like
<div id="urls" data-url="#Url.Action("ActionMethodName","YourControllerName")"></div>
Then in your get code get that stored url
$.getJSON($("#urls").data("url"),
To elaborate further this code will work on any environment (production, debug, iis, any location) without any developer worry or tweaking with config files or any other process. Tomorrow if you need to host your site on "osportal" not "bamportal" no changes need to be made, this should not be part of your code base.
One of the biggest benefits is that if the controller or action ever change the compiler will let you know that the url does not exist and you can fix it. Hardcoding paths/urls/location is a very bad/unmaintainable practice.
Related
Currently developing a ASP MVC site.
Some sections of the site will use VueJS for displaying some list, forms etc.
The project setup is Bower, Grunt, standard C# ASP project using TypeScript.
This is my first time using Vue, and the simple stuff is pretty stragt forward. Seting up a page with a form, getting data from a WebService etc.
My problem/question is, what, and how, do i get the best setup, for using Single File Components (Vue) in my cshtml view files.
So, lets say I have a section on my site, where i want to display orders from the user.
Layout, navigation etc is setup by my excisting ASP code. I have a CSHTML viewpage for the current page, pretty vanilla:
#inherits MyViewPage<MyViewModel>
#{
Layout = "~/Views/layout.cshtml";
}
<div id"app">
</div>
Thats it for the excisting view page. In this page, i want to include a Vue Single File Component.
Previously i had the markup directly in the CSHTML page, which works fine. But when i want to user Vue-router, it becomes a problem to maintain the different views. So i should move the markup into a Component.
This is the basic setup;
const page1 = { template: '<div>Page1</div>' }
const page2 = { template: '<div>Page2</div>' }
const routes = [
{ path: '/', component: page1 },
{ path: '/page2', component: page2 }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
router,
el: "#app"
})
Lets say i create a .vue file called page1.vue instead. This contains
<template>
my new page
</template>
<script>
export default {
name: '?',
date: function() {
}
}
</script>
How do i get this file included in my CSHTML file for instance?
You need to develop with webpack to build Single File Components.
See the Vue documentation on this.
Use the Vue Cli and drop a web pack build into your cshtml page.
I'm getting 404 errors as there is no /favicon.ico. The actual icon is located as /content/favicon.ico.
I've set this to my html pages:
<link rel="icon" href="#Url.Content("~/content/favicon.ico")" type="image/x-icon" />
It works, but some browsers seem to ignore it, or look for /favicon.ico anyway.
So, what I'm asking for is an ASP route that turns "/favicon.ico" into "/content/favicon.ico".
Some browsers look for favicons in the root because thats where they used to live by convention.
Since routes are not used for static content like a favicon image, adding a route won't help. You could rewrite these requests using the IIS rewriting module or implement a custom handler for this specific case, but just putting the favicon in the root a lot less hassle - keep it simple.
I solved it by simply rewriting the target location directly in my code. Without any need of external re-write mods, as suggested on similar questions.
// Register event-listener in Web-App constructor
this.PreRequestHandlerExecute += new EventHandler(MvcApplication_PreRequestHandlerExecute);
void MvcApplication_PreRequestHandlerExecute(object sender, EventArgs e)
{
string originalPath = HttpContext.Current.Request.Path.ToLower();
if (originalPath == "/favicon.ico")
{
Context.RewritePath("~/content/favicon.ico");
}
}
MSDN documentation: http://msdn.microsoft.com/en-us/library/system.web.httpserverutility.mappath(v=vs.110).aspx
I'm planning to build a SPA with asp.net MVC4 but I'm not quite sure how I have to Setup my Project because of the Routing. Most SPA's work with hashrouting like this mypage/#/News/today for instance.
What would happen if the browses directly to mypage/News/today if I haven't specified a Controller named News with an action today?
The App should handle both types of Routing, how can I achieve this?
Do I have to build my App in a classic way, like Adding several Controllers with appropriate Actions and views and also build a clientside MVC structure with knockout, jquery etc?
You'll have to let all routes to "pages" fall through to let your SPA handle them (including essentially fake 404s if it's not to a real page in your SPA), but at the same time, need to make sure that you get the correct responses for API calls and/or file requests.
Below is the setup I have (I am using Vue as the js framework but that doesn't matter much for this, and not at all for the server-side piece).
First, add this to your Startup.cs, in addition to your default route setup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.Use(async (context, next) =>
{
await next();
var path = context.Request.Path.Value;
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page
if (context.Response.StatusCode == (int)HttpStatusCode.NotFound && !Path.HasExtension(path) && !path.StartsWith("/api"))
{
context.Request.Path = "/Home/SpaRedirect"; // attempts to redirect to the URL within the SPA
context.Response.StatusCode = (int)HttpStatusCode.OK; // Make sure we update the status code, otherwise it returns 404
await next();
}
});
...
}
So the newly added SpaRedirect to HomeController looks like this, and just stores the requested URL in ViewData...
public IActionResult SpaRedirect()
{
ViewData["RequestUrl"] = HttpContext.Request.Path;
return View("Index");
}
Then in Index.cshtml, just capture that requested url in session storage so we have it available on the client-side:
<script src="~/dist/main.js" asp-append-version="true">
sessionStorage.setItem("redirectAttempt", #ViewData["RequestUrl"]);
</script>
Then in your boot script file (the entry-point for your SPA), add something like:
let redirectAttemptUrl = sessionStorage.getItem("redirectAttempt");
if (redirectAttemptUrl) {
router.push(redirectAttemptUrl);
sessionStorage.removeItem("redirectAttempt");
}
Which just checks for the presence of a requested url, and then the SPA's router attempts to navigate to it (in the example above it is a vue-router), then removes it from storage.
So this way, if a user attempts to navigate directly to a URL by entering it in the url bar (or via a bookmark) the app will load and take them to the right place, IF it exists... which takes us to the last piece...
Finally, you have to handle "404s" within your SPA, which is done by adding a catch-all route to your routes defs that takes user to a 404 component page you set up, which for Vue would look like this:
// adding an explicit 404 path as well for programmatically handling when something is not found within the app, i.e. return this.$router.push('/404')
{ path: '/404', component: NotFound, name: '404', alias: '*' }, // remove alias to not show the actual url that resulted in our little 404 here
{ path: '*', redirect: '/404' }, // this is the catch-all path to take us to our 404 page
Caveat: I'm no expert so could be missing something, would love additional comments on how to improve this. One thing that this doesn't handle is if the user is ALREADY in the SPA and for some reason edits the URL directly to navigate to someplace else, it would still trigger a server call and full reload, which ideally wouldn't be the case, but this is a pretty trivial issue I'd say.
Ok. I have a url setup to log a user out. On the server, there is no html. The session on the server simply gets destroyed, and then the user is redirected to an address.
This works fine with plain html, but with Angular i am having issues. I've been routing all main routes using $routeProvider.when('/foo', {templateUrl: '/foo.html', controller: 'Ctrl'}) and that works fine for normal templated routes.. however, if there is no template it will not work.
So, how do i support the route /logout in the same fashion as above, when there is no html template?
A workaround is to use template instead of templateUrl. From the Angular docs:
template – {string=} – html template as a string that should be used
by ngView or ngInclude directives. this property takes precedence over
templateUrl.
This can be used as follows:
$routeProvider.when("/foo", {template: " ", controller: "Ctrl"});
Note: You must use " " instead of an empty string "" because Angular uses an if (template) check before firing the controller, and an empty string evaluates to false.
-- EDIT --
A better way to do it is to use the resolve map. See the Angular Docs:
resolve - {Object.=} - An optional map of
dependencies which should be injected into the controller.
This can be used like this:
$routeProvider.when('/foo', {resolve: {redirect: 'RedirectService'}});
Note: I've changed it from "Ctrl" to "RedirectService", because what you're describing in the question isn't really a "controller" in the Angular sense. It doesn't set up scope for a view. Instead, it's more like a service, which ends up redirecting.
I am writing the solution based on the already accepted answer and the github issue mentioned in it's comments.
The approach I am using is a resolve parameter in the $routeProvider. In my case I was trying to create a nice solution to logout in my application, when user goes to /logout.
Example code of $routeProvider:
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.
...
when('/logout', {
resolve: {
logout: ['logoutService', function (logoutService) {
logoutService();
}]
},
}).
...
}]);
In the resolve part you specify a service (factory) by name and later on you have to call it. Still it is the nicest solution around.
To make the example complete I present my logoutService:
angular.module('xxx').factory('logoutService', function ($location, Auth) {
return function () {
Auth.setUser(undefined);
$location.path('/');
}
});
Works great!
I've a bit of an odd issue with FCKEditor in my MVC project.
I've essentially got a View which renders a Partial View containing my FCKEditor (javascript, html and any other bits to make my control reusable throught my app)
I'm calling FCKEditor by doing the following:
<script src="<%= Url.Content("~/Scripts/fckeditor/fckeditor.js") %>" type="text/javascript" ></script>
<script type="text/javascript">
window.onload = function () {
var sBasePath = '<%= Url.Content("~/Scripts/fckeditor/") %>';
var oFCKeditor = new FCKeditor('FckEditor1');
oFCKeditor.BasePath = sBasePath;
oFCKeditor.ReplaceTextarea();
}
</script>
To validate my form, I'm using jQuery.Validate with the following code:
//FORM VALIDATION
$("#NewPageForm").validate({
errorContainer: "#errorblock-div1, #errorblock-div2",
errorLabelContainer: "#errorblock-div2 ul",
wrapper: "li",
rules: {
titleTxt: "required",
FckEditor1: "required"
},
messages: {
titleTxt: "You must enter a page title.",
FckEditor1: "You must enter at least some content."
},
submitHandler: function (form) {
form.submit();
}
});
Locally, when debugging my project, this works absolutely fine and throws up an error to the user when they leave the editor blank.
However, when I publish the application and run it from the server, whenever the editor has content in, the form won't validate, telling me that my FCKEditor control is empty, when it clearly isn't.
If I take away the validation, and submit the form with all the relevant boxes filled, the collection of collection("FckEditor1") is also empty, but again, running this locally works first time, every time.
I've had a search around and what seems to be happening is that my text area <%= Html.TextArea("FckEditor1", New With {.name = "FckEditor1", .class = "required"})%> isn't being populated with the content entered into the FCKEditor, even though this is the box that FCK is linked to.
The issue exists in all the browsers I've tried (IE8 and FF).
Not sure if it's also worth noting that FCK still renders fine too?
I'm not entirely convinced it's an issue with my code as it runs perfect locally. Is there anything I need to configure server-side or in web.config that could be causing it?
Has anyone else come across this or have any ideas as to how to solve the issue?
EDIT
I think I could be partway there with a fix.
I've added the following so far:
$('#publishBtn').button().click(function () {
if (typeof (FCKeditorAPI) == "object") {
FCKeditorAPI.GetInstance("FckEditor1").UpdateLinkedField();
} else {
alert('this is not an object!');
}
if ($("#FckEditor1").val() == "") {
alert("FCKEditor is empty");
} else {
$("#NewPageForm").submit();
}
});
Though, I now get sent to a blank page when the form is submitted, though nothing in my controller seems to be the issue - at least I don't think so as if there was an issue there, it'd do the same on my local machine.
One thing that may be causing your issue if you are validating on the server is that once it's on IIS you cannot post HTML content without the request validation preventing the FCKContent from being passed. Cassini wont check this so it works on your local machine.
try adding this to your the action which is called when you post to validate:
[ValidateInput(false)]
public ActionResult UpdateAction()
{
}
If possible, use CKEditor instead; it integrates with MVC much better.
http://ckeditor.com/
http://ckeditor.com/what-is-ckeditor
I found the source of my problem, and it's to do with the way I'm handling multiple submit buttons in my controller.
I've tweaked it and it's now fixed.