How do you get the current area name in the view or controller?
Is there anything like ViewContext.RouteData.Values["controller"] for areas?
From MVC2 onwards you can use ViewContext.RouteData.DataTokens["area"]
HttpContext.Current.Request.RequestContext.RouteData.DataTokens["area"]
You can get it from the controller using:
ControllerContext.RouteData.DataTokens["area"]
In ASP.NET Core 1.0 the value is found in
ViewContext.RouteData.Values["area"];
I just wrote a blog entry about this, you can visit that for more details, but my answer was to create an Extension Method, shown below.
The key kicker was that you pull the MVC Area from the .DataTokens and the controller/action from the .Values of the RouteData.
public static MvcHtmlString TopMenuLink(this HtmlHelper htmlHelper, string linkText, string controller, string action, string area, string anchorTitle)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var url = urlHelper.Action(action, controller, new { #area = area });
var anchor = new TagBuilder("a");
anchor.InnerHtml = HttpUtility.HtmlEncode(linkText);
anchor.MergeAttribute("href", url);
anchor.Attributes.Add("title", anchorTitle);
var listItem = new TagBuilder("li");
listItem.InnerHtml = anchor.ToString(TagRenderMode.Normal);
if (CheckForActiveItem(htmlHelper, controller, action, area))
listItem.GenerateId("menu_active");
return MvcHtmlString.Create(listItem.ToString(TagRenderMode.Normal));
}
private static bool CheckForActiveItem(HtmlHelper htmlHelper, string controller, string action, string area)
{
if (!CheckIfTokenMatches(htmlHelper, area, "area"))
return false;
if (!CheckIfValueMatches(htmlHelper, controller, "controller"))
return false;
return CheckIfValueMatches(htmlHelper, action, "action");
}
private static bool CheckIfValueMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.Values[dataToken];
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
private static bool CheckIfTokenMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.DataTokens[dataToken];
if (dataToken == "action" && item == "Index" && string.IsNullOrEmpty(routeData))
return true;
if (dataToken == "controller" && item == "Home" && string.IsNullOrEmpty(routeData))
return true;
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
Then you can implement it as below :
<ul id="menu">
#Html.TopMenuLink("Dashboard", "Home", "Index", "", "Click here for the dashboard.")
#Html.TopMenuLink("Courses", "Home", "Index", "Courses", "List of our Courses.")
</ul>
I created an extension method for RouteData that returns the current area name.
public static string GetAreaName(this RouteData routeData)
{
object area;
if (routeData.DataTokens.TryGetValue("area", out area))
{
return area as string;
}
return null;
}
Since RouteData is available on both ControllerContext and ViewContext it can be accessed in your controller and views.
It is also very easy to test:
[TestFixture]
public class RouteDataExtensionsTests
{
[Test]
public void GetAreaName_should_return_area_name()
{
var routeData = new RouteData();
routeData.DataTokens.Add("area", "Admin");
routeData.GetAreaName().ShouldEqual("Admin");
}
[Test]
public void GetAreaName_should_return_null_when_not_set()
{
var routeData = new RouteData();
routeData.GetAreaName().ShouldBeNull();
}
}
There is no need to check if RouteData.DataTokens is null since this always initialized internally.
Get area name in View (.NET Core 2.2):
ViewContext?.ActionDescriptor?.RouteValues["area"]
MVC Futures has an AreaHelpers.GetAreaName() method. However, use caution if you're using this method. Using the current area to make runtime decisions about your application could lead to difficult-to-debug or insecure code.
I know this is old, but also, when in a filter like ActionFilter, the context does not easily provide you with the area information.
It can be found in the following code:
var routeData = filterContext.RequestContext.RouteData;
if (routeData.DataTokens["area"] != null)
area = routeData.DataTokens["area"].ToString();
So the filterContext is being passed in on the override and the correct RouteData is found under the RequestContext. There is a RoutData at the Base level, but the DataTokens DO NOT have the area in it's dictionary.
To get area name in the view, in ASP.NET Core MVC 2.1:
#Context.GetRouteData().Values["area"]
I dont know why but accepted answer is not working. It returns null with e.g ( maybe about mvc, i use .net core )
http://localhost:5000/Admin/CustomerGroup
I always debug the variable and fetch data from in it.
Try this. It works for me
var area = ViewContext.RouteData.Values["area"]
Detailed logical example
Layout = ViewContext.RouteData.Values["area"] == null ? "_LayoutUser" : "_LayoutAdmin";
Asp .Net Core 3.1
Scenario: I wanted to retrieve the current area name in a ViewCompnent Invoke method.
public IViewComponentResult Invoke()
{
string areaName = this.RouteData.Values["area"];
//Your code here...
return View(items);
}
I know this is a very very old post but we can use the Values Property exactly the same way as the DataTokens
Url.RequestContext.RouteData.Values["action"] worked for me.
In MVC 5, this seems to be needed now. When in a Controller, pass this.ControllerContext.RouteData to this routine:
/// <summary>
/// Get the name of the Area from the RouteData
/// </summary>
/// <param name="routeData"></param>
/// <returns></returns>
private static string GetArea(RouteData routeData)
{
var area = routeData.DataTokens["area"]?.ToString();
if (area != null)
{
// this used to work
return area;
}
// newer approach
var matchedList = routeData.Values["MS_DirectRouteMatches"] as List<RouteData>;
if (matchedList != null)
{
foreach (var matchedRouteData in matchedList)
{
if (matchedRouteData.DataTokens.TryGetValue("area", out var rawArea))
{
return rawArea.ToString();
}
}
}
return "";
}
Related
While implementing a WebApplication we came across an dubious entry in the RouteValueDictionary with the key "RouteModels".
Even Google doesn't give me results for that entry.
Can someone explain where it comes from?
Does it come from the custom routes?
Or are this the values bound by modelbinding?
The declaration of the ActionMethode at the controller is:
[Route("User/{user}")]
public ActionResult UserForums(User user, int page = 1)
The RouteValueDictionary of the Request contains the following entries:
Does it come from the custom routes?
Unclear. However, route values are completely dependent on how a custom route class is designed.
public class SampleRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path.Substring(1);
if (path.Equals("the/virtual/path"))
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["user"] = "TheUser";
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Custom";
routeData.Values["RouteModels"] = new List<RouteModel> { new RouteModel { Name = "Foo" }, new RouteModel { Name = "Bar" } };
routeData.DataTokens["MetaData"] = "SomeMetadata";
return routeData;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
string user = values["user"] as string;
string controller = values["controller"] as string;
string action = values["action"] as string;
IEnumerable<RouteModel> routeModels = values["RouteModels"] as IEnumerable<RouteModel>;
if ("TheUser".Equals(user) && "Home".Equals(controller) && "Custom".Equals(action))
{
// Use the route models to either complete the match or to do something else.
return new VirtualPathData(this, "the/virtual/path");
}
return null;
}
private class RouteModel
{
public string Name { get; set; }
}
}
My guess is that this is either some specialized route or filter that is storing extra data in the route collection. But why the author chose to store it as route values rather than DataTokens (for metadata) is a mystery.
I have a custom RouteBase, MyRoute which I want to work for an area "MyArea" which contains code like:
public override GetRouteData(HttpContextBase httpContext)
{
var result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("area", "MyArea");
result.Values.Add("controller", "MyController");
result.Values.Add("action", "Index");
}
I register this in the MyAreaAreaRegistration.cs file:
public override string AreaName { get { return "MyArea"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(new MyRoute());
// other routes
context.MapRoute(/* ... */);
}
and when making requests, it successfully calls the Index action on MyController:
public ActionResult Index()
{
return this.View();
}
However, MVC doesn't search in the correct folders for the view:
The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/MyController/Index.aspx
~/Views/MyController/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/MyController/Index.cshtml
~/Views/MyController/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml
when the view is located in
~/Areas/MyArea/Views/MyController/Index.cshtml
How can I make MVC search in the correct area?
If you look at the source for AreaRegistrationContext.MapRoute you can see that it treats the area differently to the other route variables:
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (namespaces == null && this.Namespaces != null)
{
namespaces = this.Namespaces.ToArray<string>();
}
Route route = this.Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = this.AreaName; // *** HERE! ***
bool flag = namespaces == null || namespaces.Length == 0;
route.DataTokens["UseNamespaceFallback"] = flag;
return route;
}
where this.AreaName is populated from the AreaRegistration.
So a quick fix for the problem is to replace the call:
result.Values.Add("area", "MyArea");
with
result.DataTokens["area"] = "MyArea";
I've configured my ASP.NET MVC5 application to use AttributeRouting for WebApi:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
I have an ApiController as follows:
[RoutePrefix("api/v1/subjects")]
public class SubjectsController : ApiController
{
[Route("search")]
[HttpPost]
public SearchResultsViewModel Search(SearchCriteriaViewModel criteria)
{
//...
}
}
I would like to generate a URL to my WebApi controller action without having to specify an explicit route name.
According to this page on CodePlex, all MVC routes have a distinct name, even if it is not specified.
In the absence of a specified route name, Web API will generate a
default route name. If there is only one attribute route for the
action name on a particular controller, the route name will take the
form "ControllerName.ActionName". If there are multiple attributes
with the same action name on that controller, a suffix gets added to
differentiate between the routes: "Customer.Get1", "Customer.Get2".
On ASP.NET, it doesn't say exactly what is the default naming convention, but it does indicate that every route has a name.
In Web API, every route has a name. Route names are useful for
generating links, so that you can include a link in an HTTP response.
Based on these resources, and an answer by StackOverflow user Karhgath, I was led to believe that the following would produce a URL to my WebApi route:
#(Url.RouteUrl("Subjects.Search"))
However, this produces an error:
A route named 'Subjects.Search' could not be found in the route
collection.
I've tried a few other variants based on other answers I found on StackOverflow, none with success.
#(Url.Action("Search", "Subjects", new { httproute = "" }))
#(Url.HttpRouteUrl("Search.Subjects", new {}))
In fact, even providing a Route name in the attribute only seems to work with:
#(Url.HttpRouteUrl("Search.Subjects", new {}))
Where "Search.Subjects" is specified as the route name in the Route attribute.
I don't want to be forced to specify a unique name for my routes.
How can I generate a URL to my WebApi controller action without having to explicitly specify a route name in the Route attribute?
Is it possible that the default route naming scheme has changed or is documented incorrectly at CodePlex?
Does anyone have some insight on the proper way to retrieve a URL for a route that has been setup with AttributeRouting?
Using a work around to find the route via inspection of Web Api's IApiExplorer along with strongly typed expressions I was able to generate a WebApi2 URL without specifying a Name on the Route attribute with attribute routing.
I've created a helper extension which allows me to have strongly typed expressions with UrlHelper in MVC razor. This works very well for resolving URIs for my MVC Controllers from with in views.
Home
<li>#(Html.ActionLink<AccountController>("Sign in", c => c.Signin(null)))</li>
<li>#(Html.ActionLink<AccountController>("Create an account", c => c.Signup(), htmlAttributes: null))</li>
#using (Html.BeginForm<ToolsController>(c => c.Track(null), FormMethod.Get, htmlAttributes: new { #class = "navbar-form", role = "search" })) {...}
I now have a view where I am trying to use knockout to post some data to my web api and need to be able to do something like this
var targetUrl = '#(Url.HttpRouteUrl<TestsApiController>(c => c.TestAction(null)))';
so that I don't have to hard code my urls (Magic strings)
My current implementation of my extension method for getting the web API url is defined in the following class.
public static class GenericUrlActionHelper {
/// <summary>
/// Generates a fully qualified URL to an action method
/// </summary>
public static string Action<TController>(this UrlHelper urlHelper, Expression<Action<TController>> action)
where TController : Controller {
RouteValueDictionary rvd = InternalExpressionHelper.GetRouteValues(action);
return urlHelper.Action(null, null, rvd);
}
public const string HttpAttributeRouteWebApiKey = "__RouteName";
public static string HttpRouteUrl<TController>(this UrlHelper urlHelper, Expression<Action<TController>> expression)
where TController : System.Web.Http.Controllers.IHttpController {
var routeValues = expression.GetRouteValues();
var httpRouteKey = System.Web.Http.Routing.HttpRoute.HttpRouteKey;
if (!routeValues.ContainsKey(httpRouteKey)) {
routeValues.Add(httpRouteKey, true);
}
var url = string.Empty;
if (routeValues.ContainsKey(HttpAttributeRouteWebApiKey)) {
var routeName = routeValues[HttpAttributeRouteWebApiKey] as string;
routeValues.Remove(HttpAttributeRouteWebApiKey);
routeValues.Remove("controller");
routeValues.Remove("action");
url = urlHelper.HttpRouteUrl(routeName, routeValues);
} else {
var path = resolvePath<TController>(routeValues, expression);
var root = getRootPath(urlHelper);
url = root + path;
}
return url;
}
private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {
var controllerName = routeValues["controller"] as string;
var actionName = routeValues["action"] as string;
routeValues.Remove("controller");
routeValues.Remove("action");
var method = expression.AsMethodCallExpression().Method;
var configuration = System.Web.Http.GlobalConfiguration.Configuration;
var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions
.FirstOrDefault(c =>
c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)
&& c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method
&& c.ActionDescriptor.ActionName == actionName
);
var route = apiDescription.Route;
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));
var request = new System.Net.Http.HttpRequestMessage();
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;
var virtualPathData = route.GetVirtualPath(request, routeValues);
var path = virtualPathData.VirtualPath;
return path;
}
private static string getRootPath(UrlHelper urlHelper) {
var request = urlHelper.RequestContext.HttpContext.Request;
var scheme = request.Url.Scheme;
var server = request.Headers["Host"] ?? string.Format("{0}:{1}", request.Url.Host, request.Url.Port);
var host = string.Format("{0}://{1}", scheme, server);
var root = host + ToAbsolute("~");
return root;
}
static string ToAbsolute(string virtualPath) {
return VirtualPathUtility.ToAbsolute(virtualPath);
}
}
InternalExpressionHelper.GetRouteValues inspects the expression and generates a RouteValueDictionary that will be used to generate the url.
static class InternalExpressionHelper {
/// <summary>
/// Extract route values from strongly typed expression
/// </summary>
public static RouteValueDictionary GetRouteValues<TController>(
this Expression<Action<TController>> expression,
RouteValueDictionary routeValues = null) {
if (expression == null) {
throw new ArgumentNullException("expression");
}
routeValues = routeValues ?? new RouteValueDictionary();
var controllerType = ensureController<TController>();
routeValues["controller"] = ensureControllerName(controllerType); ;
var methodCallExpression = AsMethodCallExpression<TController>(expression);
routeValues["action"] = methodCallExpression.Method.Name;
//Add parameter values from expression to dictionary
var parameters = buildParameterValuesFromExpression(methodCallExpression);
if (parameters != null) {
foreach (KeyValuePair<string, object> parameter in parameters) {
routeValues.Add(parameter.Key, parameter.Value);
}
}
//Try to extract route attribute name if present on an api controller.
if (typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)) {
var routeAttribute = methodCallExpression.Method.GetCustomAttribute<System.Web.Http.RouteAttribute>(false);
if (routeAttribute != null && routeAttribute.Name != null) {
routeValues[GenericUrlActionHelper.HttpAttributeRouteWebApiKey] = routeAttribute.Name;
}
}
return routeValues;
}
private static string ensureControllerName(Type controllerType) {
var controllerName = controllerType.Name;
if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) {
throw new ArgumentException("Action target must end in controller", "action");
}
controllerName = controllerName.Remove(controllerName.Length - 10, 10);
if (controllerName.Length == 0) {
throw new ArgumentException("Action cannot route to controller", "action");
}
return controllerName;
}
internal static MethodCallExpression AsMethodCallExpression<TController>(this Expression<Action<TController>> expression) {
var methodCallExpression = expression.Body as MethodCallExpression;
if (methodCallExpression == null)
throw new InvalidOperationException("Expression must be a method call.");
if (methodCallExpression.Object != expression.Parameters[0])
throw new InvalidOperationException("Method call must target lambda argument.");
return methodCallExpression;
}
private static Type ensureController<TController>() {
var controllerType = typeof(TController);
bool isController = controllerType != null
&& controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !controllerType.IsAbstract
&& (
typeof(IController).IsAssignableFrom(controllerType)
|| typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)
);
if (!isController) {
throw new InvalidOperationException("Action target is an invalid controller.");
}
return controllerType;
}
private static RouteValueDictionary buildParameterValuesFromExpression(MethodCallExpression methodCallExpression) {
RouteValueDictionary result = new RouteValueDictionary();
ParameterInfo[] parameters = methodCallExpression.Method.GetParameters();
if (parameters.Length > 0) {
for (int i = 0; i < parameters.Length; i++) {
object value;
var expressionArgument = methodCallExpression.Arguments[i];
if (expressionArgument.NodeType == ExpressionType.Constant) {
// If argument is a constant expression, just get the value
value = (expressionArgument as ConstantExpression).Value;
} else {
try {
// Otherwise, convert the argument subexpression to type object,
// make a lambda out of it, compile it, and invoke it to get the value
var convertExpression = Expression.Convert(expressionArgument, typeof(object));
value = Expression.Lambda<Func<object>>(convertExpression).Compile().Invoke();
} catch {
// ?????
value = String.Empty;
}
}
result.Add(parameters[i].Name, value);
}
}
return result;
}
}
The trick was to get the route to the action and use that to generate the URL.
private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {
var controllerName = routeValues["controller"] as string;
var actionName = routeValues["action"] as string;
routeValues.Remove("controller");
routeValues.Remove("action");
var method = expression.AsMethodCallExpression().Method;
var configuration = System.Web.Http.GlobalConfiguration.Configuration;
var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions
.FirstOrDefault(c =>
c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)
&& c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method
&& c.ActionDescriptor.ActionName == actionName
);
var route = apiDescription.Route;
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));
var request = new System.Net.Http.HttpRequestMessage();
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;
request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;
var virtualPathData = route.GetVirtualPath(request, routeValues);
var path = virtualPathData.VirtualPath;
return path;
}
So now if for example I have the following api controller
[RoutePrefix("api/tests")]
[AllowAnonymous]
public class TestsApiController : WebApiControllerBase {
[HttpGet]
[Route("{lat:double:range(-90,90)}/{lng:double:range(-180,180)}")]
public object Get(double lat, double lng) {
return new { lat = lat, lng = lng };
}
}
Works for the most part so far when I test it
#section Scripts {
<script type="text/javascript">
var url = '#(Url.HttpRouteUrl<TestsApiController>(c => c.Get(1,2)))';
alert(url);
</script>
}
I get /api/tests/1/2, which is what I wanted and what I believe would satisfy your requirements.
Note that it will also default back to the UrlHelper for actions with route attributes that have the Name.
According to this page on CodePlex, all MVC routes have a distinct name, even if it is not specified.
Docs on codeplex is for WebApi 2.0 beta and looks like things have changed since that.
I have debugded attribute routes and it looks like WebApi create single route for all actions without specified RouteName with the name MS_attributerouteWebApi.
You can find it in _routeCollection._namedMap field:
GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap
This collection is also populated with named routes for which route name was specified explicitly via attribute.
When you generate URL with Url.Route("RouteName", null); it searches for route names in _routeCollection field:
VirtualPathData virtualPath1 =
this._routeCollection.GetVirtualPath(requestContext, name, values1);
And it will find only routes specified with route attributes there. Or with config.Routes.MapHttpRoute of course.
I don't want to be forced to specify a unique name for my routes.
Unfortunately, there is no way to generate URL for WebApi action without specifying route name explicitly.
In fact, even providing a Route name in the attribute only seems to work with Url.HttpRouteUrl
Yes, and that is because API routes and MVC routes use different collections to store routes and have different internal implementation.
Very first thing is, if you want to access a route then definitely you need a unique identifier for that just like any other variable we use in normal c# programming.
Hence if defining a unique name for each route is a headache for you, but still I think you will have to with it because the benefit its providing is much better.
Benefit: Think of a scenario where you want to change your route to a new value but it will require you to change that value across the applciation wherever you have used it.
In this scenario, it will be helpful.
Following is the code sample to generate link from route name.
public class BooksController : ApiController
{
[Route("api/books/{id}", Name="GetBookById")]
public BookDto GetBook(int id)
{
// Implementation not shown...
}
[Route("api/books")]
public HttpResponseMessage Post(Book book)
{
// Validate and add book to database (not shown)
var response = Request.CreateResponse(HttpStatusCode.Created);
// Generate a link to the new book and set the Location header in the response.
string uri = **Url.Link("GetBookById", new { id = book.BookId });**
response.Headers.Location = new Uri(uri);
return response;
}
}
Please read this link
And yes you are gonna need to define this routing name in order to access them with the ease you want to access. The convention based link generation you want is currently not available.
One more thing I would like to add here is, if this is really very concerning issue for you then we can write out own helper methods which will take two parameters {ControllerName} and {ActionName} and will return the route value using some logic.
Let us know if you really think that its worthy to do that.
I am trying to load Razor View from database.
I follow ASP.NET MVC and virtual views and VirtualPathProvider in MVC 5 to do that.
my Code :
VirtualPathProvider :
public class DbPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var page = FindPage(virtualPath);
if (page == null)
{
return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var page = FindPage(virtualPath);
if (page == null)
{
return base.GetFile(virtualPath);
}
else
{
return new DbVirtualFile(virtualPath, page.PageData.ToArray());
}
}
private SiteModel FindPage(string virtualPath)
{
var db = new DatabaseContext();
var page = db.SiteModels.FirstOrDefault(x => x.SiteName == virtualPath);
return page;
}
}
VirtualFile
public class DbVirtualFile : VirtualFile
{
private byte[] data;
public DbVirtualFile(string virtualPath, byte[] data)
: base(virtualPath)
{
this.data = data;
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
Global.asax :
protected void Application_Start()
{
HostingEnvironment.RegisterVirtualPathProvider(new DbPathProvider());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
Action :
public ActionResult Display(string id)
{
var db = new DatabaseContext();
var site = db.SiteModels.FirstOrDefault(x => x.PageName == id);
if (site == null)
{
return RedirectToAction("Index", "Home");
}
ViewBag.Body = site.PageContent;
return View(System.IO.Path.GetFileNameWithoutExtension(site.SiteName));
}
Data:
Case 1:
When virtualPath value is "/Views/Home/Contact.cshtml" then FileExists method return true and GetFile method is called.
Case 2:
When virtualPath value is "~/Home/Display/ce28bbb6-03cb-4bf4-8820-373890396a90" then FileExists method return true and GetFile method and Display Action is never called. and result is
HTTP Error 404.0 - Not Found
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.
I have no idea about dynamic view. I just read that two article and try to implement it.
Please tell me where I am doing wrong.
I am using MVC 5 and .NET 4.5
something I should be able to help you with.
There is something you are not thinking about which is ok, because I had loads of trouble with this too.
I think the issue you have here is knowing in what order everything fires.
This is what happens:
First the VirtualPathProvider FileExists method is called and
this is where you are doing your magic to find the page. My methods are slightly different to yours, here is an example:
public IList<Page> Pages
{
get
{
using (var uow = new UnitOfWork<SkipstoneContext>())
{
var companyService = new CompanyService(uow);
var company = companyService.GetTenant();
if (company != null)
{
var pageService = new PageService(uow, company.Id);
return pageService.GetPublished();
}
}
return null;
}
}
public override bool FileExists(string virtualPath)
{
if (IsVirtualPath(virtualPath))
{
if (FindPage(virtualPath) != null)
{
var file = (PageVirtualFile)GetFile(virtualPath);
return file.Exists;
}
}
return Previous.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsVirtualPath(virtualPath))
{
var page = FindPage(virtualPath);
if (page != null)
{
var decodedString = Uri.UnescapeDataString(page.ViewData);
var bytes = Encoding.ASCII.GetBytes(decodedString);
return new PageVirtualFile(virtualPath, bytes);
}
}
return Previous.GetFile(virtualPath);
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsVirtualPath(virtualPath))
return null;
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
{
if (IsVirtualPath(virtualPath))
return Guid.NewGuid().ToString();
return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}
private Page FindPage(string virtualPath)
{
var virtualName = VirtualPathUtility.GetFileName(virtualPath);
var virtualExtension = VirtualPathUtility.GetExtension(virtualPath);
try
{
if (Pages != null)
{
var id = Convert.ToInt32(virtualName.Replace(virtualExtension, ""));
var page = Pages.Where(model => model.Id == id && model.Extension.Equals(virtualExtension, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
return page;
}
}
catch(Exception ex)
{
// Do nothing
}
return null;
}
private bool IsVirtualPath(string virtualPath)
{
var path = (VirtualPathUtility.GetDirectory(virtualPath) != "~/") ? VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(virtualPath)) : VirtualPathUtility.GetDirectory(virtualPath);
if (path.Equals("~/Views/Routing", StringComparison.OrdinalIgnoreCase) || path.Equals("/Views/Routing", StringComparison.OrdinalIgnoreCase))
return true;
else
return false;
}
Now, if this page is served from the database it is a virtual page, so the next thing that happens is that it calls your MvcHandler (in my handler I have these functions)
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
var vpp = new SkipstoneVirtualPathProvider(); // Create an instance of our VirtualPathProvider class
var requestContext = ((MvcHandler)httpContext.Handler).RequestContext; // Get our request context
var path = requestContext.HttpContext.Request.Url.AbsolutePath; // Get our requested path
var pages = vpp.Pages; // Get all the published pages for this company
if (pages != null && !string.IsNullOrEmpty(path)) // If we have any pages and we have a path
{
var page = this.MatchPage(pages, path);
if (page != null) // If we find the page
{
requestContext.RouteData.Values["controller"] = "Routing"; // Set the controller
requestContext.RouteData.Values["action"] = "Index"; // And the action
}
}
return base.BeginProcessRequest(httpContext, callback, state);
}
private Page MatchPage(IList<Page> pages, string path)
{
if (path == "/" || !path.EndsWith("/"))
{
var page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault(); // Try to match the path first
if (page == null) // If we have no page, then get the directory and see if that matches any page
{
path = VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(path));
page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
}
return page; // return our page or null if no page exists
}
return null; // return null if anything fails
}
Just in case, when you create your MvcHandler if you have not already done so, it must be registered in RouteConfig like this:
// default MVC route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
).RouteHandler = new SkipstoneRouteHandler();
In my MvcHandler I get my page and if it matches any virtual page I move to a RoutingController
public ActionResult Index()
{
using (var uow = new UnitOfWork<SkipstoneContext>())
{
var userId = User.Identity.GetUserId();
var service = new PageService(uow, this.CompanyId);
var userService = new UserService(uow, this.CompanyId);
var user = userService.Get(userId);
var fileName = service.View(Request.Url, user);
if (!fileName.StartsWith(#"/") && !fileName.StartsWith("~/"))
return View(fileName); // Show the page
else
return Redirect(fileName); // Redirect to our page
}
}
just for your sake I will show the service.View method
/// <summary>
/// Views the CMS page
/// </summary>
/// <param name="uri">The current Request.Url</param>
/// <param name="user">The current User</param>
/// <returns>The filename of the requested page</returns>
public string View(Uri uri, User user)
{
var path = uri.AbsolutePath; // Get our Requested Url
var queryString = uri.Query;
var pages = this.GetPublished();
var page = pages.Where(model => model.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).SingleOrDefault(); // Try to get the page
if (page == null) // If our page is null
page = pages.Where(model => model.Path.Equals(VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(path)))).SingleOrDefault(); // try to get the page based off the directory
if (page == null) // If the page is still null, then it doesn't exist
throw new HttpException(404, "This page has been deleted or removed."); // Throw the 404 error
if (page.Restricted && user == null) // If our page is restricted and we are not logged in
return "~/Account/LogOn?ReturnUrl=" + page.Path + queryString; // Redirect to the login page
if (user != null && page.Restricted)
{
if (PageService.IsForbidden(page, user))
throw new HttpException(401, "You do not have permission to view this page."); // Throw 401 error
}
return Path.GetFileNameWithoutExtension(page.Id.ToString());
}
Then we are taken back to our VirtualPathProvider and the process begins again, but this time the virtual path will look something like this: "~/Views/Routing/1331.aspx"
If you notice, my FindPage method also looks for an extension match which I store in the database too:
Then GetFile will be called and as long as you have followed my path, you should return the VirtualFile
I really hope that helps :)
I have same issue.
Razor view engine not call GetFile if you virtualPath is not *.cshtml format.
You need generate {Guid}.cshtml path format
WARNING: ASP.NET 4 (MVC5) relative!
Despite the number of answers and a selected one, I will share my experience.
FileExists(string virtualPath)
gets called twice, once with a app relative path ("~/bla-bla") and once more with absolute one ("/bla-bla"). In my case the second one (the abs. path) fails. Seems like in ASP.NET 3 this was OK, however ASP.NET 4 (MVC5) checks it too. And requires it to return true. As a result
GetFile(string virtualPath)
doesn't get called.
For those who need to implement such behaviour VirtualPathUtility offers ToAbsolute and ToAppRelative convenience methods.
HTH
I'm developing an application and I'm securing it using Fluent Security.
I have 2 different areas within the web application, namely an Admin area and a Members area.
I implemented my Policies Violation Handlers following the guidelines in the fluent security website.
Everything works fine except that when an exception is thrown I need to redirect to an error page. These are different depending on which area you are. For instance, the ForbiddenAccess error page from the Admin area is different than the one from the Members Area.
My Implementation of IPolicyViolationHandler is the following.
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = "Admin" }));
}
}
I need to get where the error occurs so that when I redirect my area is either "
Admin" or "Members" and I can't figure out how to get that information from the exception. If I can get that information, redirecting to the appropriate page is trivial.
Any help, suggestions, or if not possible, a workaround is appreciated.
Thanks,
It is currently not possible to extract that information from FluentSecurity but it should be really easy to do anyway.
Below are two extensionmethods that should help you get the current area name.
public static string GetAreaName(this RouteData routeData)
{
object value;
if (routeData.DataTokens.TryGetValue("area", out value))
{
return (value as string);
}
return GetAreaName(routeData.Route);
}
public static string GetAreaName(this RouteBase route)
{
var areRoute = route as IRouteWithArea;
if (areRoute != null)
{
return areRoute.Area;
}
var standardRoute = route as Route;
if ((standardRoute != null) && (standardRoute.DataTokens != null))
{
return (standardRoute.DataTokens["area"] as string) ?? string.Empty;
}
return string.Empty;
}
An here's how you would use it:
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current));
var areaName = routeData.GetAreaName();
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = areaName }));
}
}
Feel free to add a feature request for it.