SiteMap.CurrentNode returns null when using query parameter - asp.net

I have written a custom ASP.NET sitemap provider, it works well but if I add a query parameter to a virtual path SiteMap.CurrentNode returns null - it does not find the page. I've put breakpoints in all my code and never once does it enter my virtual path provider with a query parameter. What am I missing here?

I found an answer to my question and post it here for later use. Seems the sitemap provider always uses the path without the querystring parameters when lookup up matching paths. The trick is to not use Reqest.RawUrl in your overriden SiteMapProvider.CurrentNode() function but rather use Request.Path ; I've posted my solution below:
public class CustomSiteMapProvider : SiteMapProvider {
// Implement the CurrentNode property.
public override SiteMapNode CurrentNode {
get {
var currentUrl = FindCurrentUrl();
// Find the SiteMapNode that represents the current page.
var currentNode = FindSiteMapNode(currentUrl);
return currentNode;
}
}
// Get the URL of the currently displayed page.
string FindCurrentUrl() {
try {
// The current HttpContext.
var currentContext = HttpContext.Current;
if (currentContext != null) return currentContext.Request.Path;
throw new Exception("HttpContext.Current is Invalid");
} catch (Exception e) {
throw new NotSupportedException("This provider requires a valid context.", e);
}
}
...

Related

MVC5 Using Controller to serve images, value can not be null

I'm new to MVC and I followed another tutorial on loading local files to the webpage and while it seems to work for others I am getting an error.
public class ImagesController : Controller
{
// GET: Images
public ActionResult SomeImage(string imageName)
{
var root = #"C:\Images\";
var path = Path.Combine(root, imageName);
path = Path.GetFullPath(path);
if (!path.StartsWith(root))
{
// Ensure that we are serving file only inside the root folder
// and block requests outside like "../web.config"
throw new HttpException(403, "Forbidden");
}
return File(path, "image/png");
}
}
The error I'm getting is:
An exception of type 'System.ArgumentNullException' occurred in mscorlib.dll but was not handled in user code
Additional information: Value cannot be null.
And the line it highlights is:
var path = Path.Combine(root, imageName);
When using Url.Action() helper, the property name in the anonymous object has to match the name of the parameter in the action.
And in your case:
#Url.Action("SomeImage", "Images", new { imageName = "Logo.png" })
As haim770 mentioned, it looks like you are missing the "Name" part of the imageName parameter. You can certainly add error handling to your code in order to localize the exception. This might help you narrow down any coding issues.
if(imageName == null)
{
throw new ArgumentNullException(null, "imageName is NULL");
}

Can this be done using ASP.NET URL Rewriting

There are about 1700 articles listed on my website created using ASP.NET 4.0 Web Forms. These articles have a url format as:
http://www.mymymyarticles.com/Article.aspx?ID=400
I have explored ASP.NET Friendly URLs as well IIS URL Rewrite. These extensions are great but once a rule is created, they treat all url's generically.
Is it possible that I manually generate my own url string for every url that exists on my website? For eg:
I want to permanent redirect http://www.mymymyarticles.com/Article.aspx?ID=400 to http://www.mymymyarticles.com/this-is-a-very-long-url whereas
http://www.mymymyarticles.com/Article.aspx?ID=500 can be redirected to http://www.mymymyarticles.com/article/short-url and
http://www.mymymyarticles.com/Article.aspx?ID=523 can be redirected to http://www.mymymyarticles.com/very-short-url
So you can see there is no uniformity in the url's that I want to manually generate. Basically I want full control over the url's. How can I go about this. Will it affect performance?
Any examples are appreciated.
Do you have a way of mapping an ID to the url of the new page? If that is the case, you could probably achieve this with ASP.NET Routing. What I would do is start with defining a route:
var route = routes.MapRoute(
"LegacyDocument",
"Articles.aspx{*pathInfo}",
null,
constraints: new { pathInfo = new LegacyDocumentRouteConstraint() }
);
route.RouteHandler = new RedirectRouteHandler();
This route merely captures any requests for /articles.aspx, but it has a constraint and a custom route handler.
The purpose of the constraint is to ensure we at least have the ID query string property and it is a number:
public class LegacyDocumentRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
// Don't both doing anything while we generate links.
return false;
}
string id = httpContext.Request.QueryString["id"];
if (string.IsNullOrWhiteSpace(id))
{
// No query string argument was provided.
return false;
}
int documentId;
if (!int.TryParse(id, out documentId))
{
// The Id is not a number.
return false;
}
// Set the output document Id in the route values.
values["documentId"] = documentId;
return true;
}
}
If the Id was not provided, or was not a number, we can't match to an existing document, so the route will be skipped over. But when the constraint is satisfied, we store a variable in the route values values["documentId"] = documentId so we can then use it again (without having to parse it from the query string again) later in the route handler:
public class RedirectRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext context)
{
int documentId = (int)context.RouteData.Values["documentId"];
return new RedirectLegacyDocumentHttpHandler(documentId);
}
private class RedirectLegacyDocumentHttpHandler : IHttpHandler
{
private int _documentId;
public RedirectHttpHandler(int documentId)
{
_documentId = documentId;
}
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
var response = context.Response;
string url = ResolveNewDocumentUrl();
if (url == null)
{
// Issue a 410 to say the document is no longer available?
response.StatusCode = 410;
}
else
{
// Issue a 301 to the new location.
response.RedirectPermanent(url);
}
}
public string ResolveNewDocumentUrl()
{
// Resolve to a new url using _documentId
}
}
}
Route handlers perform the logic of mapping from ASP.NET routing back into the IHttpHandler logic of the ASP.NET runtime. In normal MVC, this would map to the standard MvcHandler which invokes controllers, but in our case, we need only to issue a redirect.
In the route handler, we grab our document Id from the route values, and create a new HTTP handler which performs the actual redirect. You'll need to plumb in the bit which you resolve what the actual new url would be (ResolveNewDocumentUrl), but generally it will resolve the url, if the url comes back as null, we'll issue a HTTP 410 Gone response to say to clients (and more importantly crawlers) that the item is no longer there, or it will issue an HTTP 301 Permanent Redirect with the appropriate location header to the new url.
I had overcome this by creating an xml file on the server with the below schema
<URLMapper>
<Code>1</Code>
<OLDURL>%Oldurl.aspx%</OLDURL>
<NEWURL>default.aspx</NEWURL>
<PermanentRedirect>true</PermanentRedirect>
<Order>1</Order>
<Status>true</Status>
</URLMapper>
Loaded this in Application_Start Event to an application variable (in the form of a datatable).
And in the Begin Request --
void Application_BeginRequest(object sender, EventArgs e)
{
if (Application["URLMapper"] == null) return;
DataTable dtURLs = Application["URLMapper"] as DataTable;
if (dtURLs.Rows.Count == 0) return;
string OrigUrl = HttpContext.Current.Request.Url.ToString().ToLower().Replace("'", "`");
DataRow[] drFound = dtURLs.Select("Status = true and '" + OrigUrl.Trim() + "' like oldurl", "Order",DataViewRowState.CurrentRows);
if (drFound.Length == 0) return;
string OldURL = drFound[0]["OldURL"].ToString().Replace("%","");
Response.RedirectPermanent(OrigUrl.Replace(OldURL, drFound[0]["NewURL"].ToString().Trim()), true);
return;
}

Web Performance Testing context paramters

I used the MSDN guide on creating Custom Extraction Rule, which presents this example (Extract method):
public override void Extract(object sender, ExtractionEventArgs e)
{
if (e.Response.HtmlDocument != null)
{
foreach (HtmlTag tag in e.Response.HtmlDocument.GetFilteredHtmlTags(new string[] { "input" }))
{
if (String.Equals(tag.GetAttributeValueAsString("name"), Name, StringComparison.InvariantCultureIgnoreCase))
{
string formFieldValue = tag.GetAttributeValueAsString("value");
if (formFieldValue == null)
{
formFieldValue = String.Empty;
}
// add the extracted value to the web performance test context
e.WebTest.Context.Add("someNameHere", formFieldValue);
e.Success = true;
return;
}
}
}
// If the extraction fails, set the error text that the user sees
e.Success = false;
e.Message = String.Format(CultureInfo.CurrentCulture, "Not Found: {0}", Name);
}
However, I just don't know how to use access the someNameHere in the Web Test and add it to the QueryString as a parameter.
Any help would be greatly appreciated.
Right-click on the request in the web test and select "Add URL query string parameter". Alter the name as needed and into the value field enter {{someNameHere}}. The doubled curly braces call for a context parameter value to be inserted. The doubled curly braces can be used to insert the value of a context parameter into many other places in a web test. Note that strings such as text{{someNameHere}}moretext can be used to join context values to other strings.

Why is ASP.NET View Engine checking for .Mobile.chstml view path?

For my ASP.NET MVC 4 project, I'm trying to implement a custom view engine to find an "Index.cshtml" view file if one exists within a folder. Additionally, I'm throwing a 404 for all view paths that are not found.
The 404 works when a view file doesn't exist. When a view file does exist, the view engine will then try looking for a .Mobile.cshtml file using the FileExists() function. There is no .mobile.cshtml file, so it throws an exception. Why does the view engine still look for a .mobile.cshtml file when it has found the non-mobile file already?
For example, when the view engine is able to find a view path at "~/Views/About/History/Index.cshtml", it will then try finding the file "~/Views/About/History/Index.Mobile.cshtml". Below is my full code for the custom view engine.
namespace System.Web.Mvc
{
// Extend where RazorViewEngine looks for view files.
// This looks for path/index.ext file if no path.ext file is found
// Ex: looks for "about/history/index.chstml" if "about/history.cshtml" is not found.
public class CustomViewEngine : RazorViewEngine
{
public BeckmanViewEngine()
{
AreaViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}/Index.cshtml",
};
ViewLocationFormats = new[]
{
"~/Views/{1}/{0}/Index.cshtml",
};
}
// Return 404 Exception if viewpath file in existing path is not found
protected override bool FileExists(ControllerContext context, string path)
{
if (!base.FileExists(context, path))
{
throw new HttpException(404, "HTTP/1.1 404 Not Found");
}
return true;
}
}
}
I have found the answer after digging a bit in the MVC 4 source code.
The RazorViewEngine derives from BuildManagerViewEngine, and this one in turns derives from VirtualPathProviderViewEngine.
It is VirtualPathProviderViewEngine the one that implements the method FindView:
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(viewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
}
string[] viewLocationsSearched;
string[] masterLocationsSearched;
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);
if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}
That GetPath method used there will do something like this when the view path has not been cached yet:
return nameRepresentsPath
? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations)
: GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, cacheKey, ref searchedLocations);
Getting there! The interesting method is GetPathFromGeneralName, which is the one trying to build the whole path for the view and checking if that path exists. The method is looping through each of the view locations that were registered in the View Engine, updating the view path with the display mode valid for current HttpContext and then checking if the resolved path exists. If so, the view has been found, is assigned to the result, cached and the result path returned.
private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, ref string[] searchedLocations)
{
string result = String.Empty;
searchedLocations = new string[locations.Count];
for (int i = 0; i < locations.Count; i++)
{
ViewLocation location = locations[i];
string virtualPath = location.Format(name, controllerName, areaName);
DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode);
if (virtualPathDisplayInfo != null)
{
string resolvedVirtualPath = virtualPathDisplayInfo.FilePath;
searchedLocations = _emptyLocations;
result = resolvedVirtualPath;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, virtualPathDisplayInfo.DisplayMode.DisplayModeId), result);
if (controllerContext.DisplayMode == null)
{
controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode;
}
// Populate the cache for all other display modes. We want to cache both file system hits and misses so that we can distinguish
// in future requests whether a file's status was evicted from the cache (null value) or if the file doesn't exist (empty string).
IEnumerable<IDisplayMode> allDisplayModes = DisplayModeProvider.Modes;
foreach (IDisplayMode displayMode in allDisplayModes)
{
if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId)
{
DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path));
string cacheValue = String.Empty;
if (displayInfoToCache != null && displayInfoToCache.FilePath != null)
{
cacheValue = displayInfoToCache.FilePath;
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId), cacheValue);
}
}
break;
}
searchedLocations[i] = virtualPath;
}
return result;
}
You may have noticed that I havenĀ“t talked about a piece of code with the following comment (reformatted for clarity):
// Populate the cache for all other display modes.
// We want to cache both file system hits and misses so that we can distinguish
// in future requests whether a file's status was evicted from the cache
// (null value) or if the file doesn't exist (empty string).
That (and the piece of code below the comment :)) means that once MVC 4 has found the first valid path from the View Locations registered in the View Engine, it will also check if the view file for all of the additional display modes that were not tested exist, so that information can be included in the cache (although just for that view location and not all of the locations available in the view engine).
Notice also, how it is passing a lambda to each of the tested display modes for checking if the file for that mode exists:
DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(
controllerContext.HttpContext,
virtualPath,
virtualPathExists: path => FileExists(controllerContext, path));
So that explains why when you override FileExists it is also being called for the mobile view, even when it has already found the non-mobile view.
In any case, display modes can be removed the same way they can be added: by updating the DisplayModes collection when the application starts. For example, removing the Mobile display mode and leaving just the default and unspecific one (You cannot clear the collection or no view will ever be found):
...
using System.Web.WebPages;
...
protected void Application_Start()
{
DisplayModeProvider.Instance.Modes.Remove(
DisplayModeProvider.Instance.Modes
.Single(m => m.DisplayModeId == "Mobile"));
Quite a long answer but hopefully it makes sense!
Have you tried removing Mobile DisplayModeProvider. You can achieve this by running the following in Application_Start:
var mobileDisplayMode = DisplayModeProvider.Instance.Modes.FirstOrDefault(a => a.DisplayModeId == "Mobile");
if (mobileDisplayMode != null)
{
DisplayModeProvider.Instance.Modes.Remove(mobileDisplayMode);
}
THe problem that you are getting is an expected behavior because FindView method queries DisplayModeProvider.

Output Caching using BOTH varybyparam and varybycustom

I'm trying to do something which should be very simple...I have a site with a dropdown from which the user selects a group. Thereafter, the user navigates through the site using querystring arguments from menus. So I want the caching to be dependent on the querystring - this seems to work. I also want the cache to be dependent on the group that they selected.
But when the querystring is empty, neither cache element seems to work - the page is just whatever the version was for the last selected group. My cache directive looks like this:
<%# OutputCache Duration="300" VaryByCustom="currentAtomId" VaryByParam="documentId;folderId;sectionId;renderMode;typeId" %>
My varyByCustom code looks like this:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
switch (custom)
{
case "currentAtomId":
var currentAtomId = SecurityManifold.Create().CurrentAtomId;
var returnString = currentAtomId == null ? Guid.NewGuid().ToString() : currentAtomId.ToString();
return returnString;
default:
throw new ArgumentException(string.Format("Argument '{0}' is not a valid cache argument.", custom));
}
}
The call to CurrentAtomId boils down to this:
public static int? GetCurrentAtomIdFromContext(HttpContext context)
{
int entityId;
if (context.Session == null)
{
throw new InvalidOperationException("Session is null");
}
var sessionEntityId = context.Session["CurrentEntityId"];
if (sessionEntityId == null || string.IsNullOrEmpty(sessionEntityId.ToString()))
{
return null;
}
if (!int.TryParse(sessionEntityId.ToString(), out entityId))
{
return null;
}
return entityId;
}
Finally, the code which specifies the CurrentEntityId is this:
var selectedEntityId = this.lstSecurityEntities.SelectedValue;
if (string.IsNullOrEmpty(selectedEntityId))
{
return;
}
Session["CurrentEntityId"] = selectedEntityId;
var possibleQueryString = Request.QueryString.ToString();
if (!string.IsNullOrEmpty(possibleQueryString))
{
possibleQueryString = "?" + possibleQueryString;
}
Response.Redirect("default.aspx" + possibleQueryString);
I'm baffled. Any thoughts would be appreciated.
I eventually determined the problem - when output caching is placed at a PAGE level (as opposed to a control level), the session is not available, and throws an exception. Because this exception is occurring in Global ABOVE the global error handler, it fails silently. I eventually figured this out by wrapping a try-catch block around the cache key generation code in VaryByCustomString and Response.Write-ing it out.
What a beatdown...at any rate, the solution is to implement caching at the control level, which unfortunately is a lot more work because the pieces of the page work together...but it's better than no caching. I hope this helps save somebody else some time.
Bottom Line: for varyByCustomString in global.asax - SESSION IS NOT AVAILABLE WHEN CACHING AT THE PAGE LEVEL.

Resources