I'm using MVC 5 and I'm calling an Action from a View via Html.RenderAction(). I want to pass an ID parameter to the Action and I do it like so:
Html.RenderAction("MyAction", "MyController", new { id = resourceID });
My Action is something like this: public ActionResult MyAction(int id)
This works fine. However, I want to decorate the Action with my own custom AuthorizeAttribute in order to check if the current user has access to the resource with this specific ID. In my custom AuthorizeAttribute class I have something like this:
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!base.AuthorizeCore(httpContext))
{
return false;
}
RouteData rd = httpContext.Request.RequestContext.RouteData;
var resourceID = rd.Values["id"];
SessionInfo sessionInfo = HttpContext.Current.Session["SessionInfo"] as SessionInfo;
int userID = sessionInfo.UserInfo.UserID;
return AuthorizationBusiness.DoesUserHaveAccessToResource(userID, int.Parse(resourceID.ToString()));
}
The problem is that the original Http request does not contain the id parameter, so resourceID will be null here. In order to workaround this problem, I added this parameter to the Request before calling RenderAction, like this:
Context.Request.RequestContext.RouteData.Values["id"] = resourceID;
Html.RenderAction("MyAction", "MyController", new { id = resourceID });
Now I can retrieve the ID within the AuthorizeAttribute, however, this doesn't feel like the right way to do it. What's the best way to achieve this? And can you tell me if there are any downsides in the way I'm doing it?
Thanks in advance!
If I understand correctly:
You are calling Html.RenderAction("MyAction", "MyController", new { id = resourceID });
This return a view on which Html.Action method is called
Action which is called by Html.Action is decorated with custom authorization attribute in order to check if the user is authorized to access it
If my understanding is correct then I would not implement additional authorize attribute but in the action which is called by Html.RenderAction("MyAction", "MyController", new { id = resourceID }) check if the user is authorized and if not modify the returned view so that the Html.Action won't be called.
I have an app with many widgets and their content depends on the user requesting specific route. Simply put: if widget action is requested, its content must be rendered, otherwise it's empty. Consider routes/actions like this:
~/MyApp/Index -> without model; app HTML, without any widgets
~/MyApp/Foo/{id} -> uses FooModel; if ModelState is valid, returns
Index HTML with injected partial view of Foo's widget to div#foo;
otherwise redirects to Index.
~/MyApp/Bar/{id} -> same as Foo, but different model and widget
My foo action :
public ActionResult Foo(string id) {
if (ModelState.IsValid) {
var response = FooService.GetData(id);
// Inject Foo widget to Index
}
return RedirectToAction("Index");
}
I know that it is possible to use ViewBag or other means to send variables and using the condition to decide whether to render partial view or not. But... there should be a better way to do this, right?
I use MVC's Html.RenderActionResult when I want to build shared views with non-trivial binding logic (calling the database, composing complex objects, etc). The binding logic for each widget is contained in a PartialViewResult method, which is called from the *.cshtml file using Html.RenderAction().
ContentController:
public ActionResult Index(int id)
{
var indexViewModel = new IndexViewModel
{
Id = id,
Title = "My Title",
SubHeader = "Wow its 2016"
};
return View(indexViewModel);
}
public PartialViewResult PopularContent(int id)
{
var popularContentViewModel = new List<PopularContentViewModel>();
// query by id to get popular content items
return PartialView("_PopularContent", popularContentViewModel);
}
public PartialViewResult Widget2(int id)
{
return PartialView("_Widget2Partial");
}
Index.cshtml:
#model StackOverflow.RenderAction.ViewModels.IndexViewModel
<h1>#Model.Title</h1>
<h2>#Model.SubHeader</h2>
--RenderAction will call out to the specified route.
--Note the use of the Id parameter from the viewmodel.
#{Html.RenderAction("PopularContent", "Content", new {Model.Id});}
ASP.NET MVC Attribute Routing could a be a nice solution for this:
In your controller:
public class WidgetController : Controller
{
[Route("myapp/foowidget", Name = "FooWidget")]
public ActionResult FooWidget()
{
//create any model and return any view or partial or redirect
}
[Route("myapp/boowidget/{id:int}", Name = "BooWidget")]
public ActionResult BooWidget(int id)
{
//create any model and return any view or partial or redirect
}
}
And then in a View, you can call the Route by name:
#Url.RouteUrl("FooWidget")
or
#Url.RouteUrl("BooWidget")
or
#Html.RenderPartial("FooWidget")
#Url.RouteUrl("BooWidget") will render or concatenate the id that is in current url, if url is /myapp/something/id, because of your Route attribute definition: "myapp/boowidget/{id:int}". In fact #Url.RouteUrl("BooWidget") might extract the id from any current url of the format /controllerName/action/id, though you will have to test for sure.
And notice how you can have a separation of concerns with your WidgetController and your url Routes are not dependent on that controller's name in any way. That is a nice feature of Attribute Routing, you can declare custom routes as well as organize your controllers and break from nameing convention dependency of a controllerName being part of the url controllerName/action a user sees in their browser.
In regards to Html.RenderPartial, I am not sure if RenderPartial "connects" or will be able to route to your RouteName like "FooWidget". If it does great.
If not your solution is this:
public class WidgetController : Controller
{
public ActionResult FooWidget()
{
//model, you choose, return a partial
}
public ActionResult RedirectUser()
{
//do a redirect
}
public ActionResult BooWidget()
{
//any model, any partial
}
public ActionResult BooWidget(int id)
{
//any model, any partial
}
}
Each method in your controller is single purpose, has a distinct signature and does one thing, no conditions to pass in and no decisions required.
Can I serve a cshtml page as a view without writing an action method?
So, if I have a controller called Help and a view called Money.cshtml I want to access it as localhost/help/money without writing an action method?
You could do something like the following.
Define a route that maps to just one action e.g.:
routes.MapRoute(
"Help_default", // Route name
"help/{path}", // URL with parameters
new { controller = "About", action = "Page" }
);
Then your help controller could look like the following. It basically just grabs the path from the URL and passes that as the model to the view.
public class HelpController : Controller
{
public ViewResult Page(string path)
{
return View("Page", path);
}
}
Your Page view could then look like:
#model string
#{
string viewPath = string.Format("~/Views/Help/{0}.cshtml", Model);
ViewEngineResult result = ViewEngines.Engines.FindView(this.ViewContext.Controller.ControllerContext, viewPath, null);
if (result.View != null)
{
#Html.Partial(viewPath)
} else {
// Define a not found view in the shared folder?
#Html.Partial("NotFound")
}
}
Which basically checks to see if a view exists with that path or not. It feels a bit dirty but I think it would work.
I am working on a project in asp.net mvc3(c#).
I need a solution for convert a view(not a partial view) to string from different controllers.
Code Explantion:
1) Calling "details" action of proposalmoduleController from proposalsController.
2) proposalmoduleController action "details" returns a view and convert this view(return result) as a string in proposalsController.
Code
public class proposalmoduleController : ControllerBase
{
[HttpGet]
public ActionResult details(int id, int widgetuniqueid)
{
//id - widgetid of div container
List<ModuleViewModel> listmoduleviewmodel = new List<ModuleViewModel>();
List<ModuleFieldViewModel> listmodulefieldviewmodel = new List<ModuleFieldViewModel>();
var objProposalModuleService = new ProposalModuleService();
var objModuleViewModel = new ModuleViewModel();
string WidgetTitle = "";
Int64 ModuleTemplateID = 0;
//objModuleViewModel.ProposalID = proposalid;
objModuleViewModel.ProposalModuleWidgetID = id;
listmoduleviewmodel=objProposalModuleService.Select(1, objModuleViewModel,out listmodulefieldviewmodel, out WidgetTitle, out ModuleTemplateID);
return View(listmoduleviewmodel);
}
}
public class proposalsController : ControllerBase
{
public string SaveHtml(int ProposalID)
{
var objProposalSortOrderViewModelList = new List<ProposalSortOrderViewModel>();
proposalmoduleController objModuleController = new proposalmoduleController(); // Initilize the object of proposalmoduleController for accessing details method
objProposalSortOrderViewModelList = GetProposalSortorders(ProposalID);
string result;
foreach (var item in objProposalSortOrderViewModelList)
{
ViewResult viewResult = (ViewResult)objModuleController.details(Convert.ToInt32(item.KeyID), Convert.ToInt32(item.SortOrder)); // Fetch the result returned from proposalmodulecontroller,details action
result=viewResult.ToString(); // Need to get result fetch from the proposalmodulecontroller,details action as a string
}
}
}
enter code here
Please suggest any solution.
A ViewResult is not a View. ViewResult is used by the MVC engine to determine the view that must be rendered.
I think it's better if you change your perspective:
if you want to include a partial view in a view just work on the presentation code using #Html.Partial
if you want to get the details data in your proposalsController don't call the action of the proposalmoduleController but call a service method that gives you the data
In ASP.NET MVC you can return a redirect ActionResult quite easily:
return RedirectToAction("Index");
or
return RedirectToRoute(new { controller = "home", version = Math.Random() * 10 });
This will actually give an HTTP redirect, which is normally fine. However, when using Google Analytics this causes big issues because the original referrer is lost, so Google doesn't know where you came from. This loses useful information such as any search engine terms.
As a side note, this method has the advantage of removing any parameters that may have come from campaigns but still allows me to capture them server side. Leaving them in the query string leads to people bookmarking or twitter or blog a link that they shouldn't. I've seen this several times where people have twittered links to our site containing campaign IDs.
Anyway, I am writing a 'gateway' controller for all incoming visits to the site which I may redirect to different places or alternative versions.
For now I care more about Google for now (than accidental bookmarking), and I want to be able to send someone who visits / to the page that they would get if they went to /home/7, which is version 7 of a homepage.
Like I said before if I do this I lose the ability for google to analyse the referrer:
return RedirectToAction(new { controller = "home", version = 7 });
What I really want is a
return ServerTransferAction(new { controller = "home", version = 7 });
which will get me that view without a client side redirect.
I don't think such a thing exists, though.
Currently the best thing I can come up with is to duplicate the whole controller logic for HomeController.Index(..) in my GatewayController.Index Action. This means I had to move 'Views/Home' into 'Shared' so it was accessible. There must be a better way.
How about a TransferResult class? (based on Stans answer)
/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : ActionResult
{
public string Url { get; private set; }
public TransferResult(string url)
{
this.Url = url;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var httpContext = HttpContext.Current;
// MVC 3 running on IIS 7+
if (HttpRuntime.UsingIntegratedPipeline)
{
httpContext.Server.TransferRequest(this.Url, true);
}
else
{
// Pre MVC 3
httpContext.RewritePath(this.Url, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(httpContext);
}
}
}
Updated: Now works with MVC3 (using code from Simon's post). It should (haven't been able to test it) also work in MVC2 by looking at whether or not it's running within the integrated pipeline of IIS7+.
For full transparency; In our production environment we've never use the TransferResult directly. We use a TransferToRouteResult which in turn calls executes the TransferResult. Here's what's actually running on my production servers.
public class TransferToRouteResult : ActionResult
{
public string RouteName { get;set; }
public RouteValueDictionary RouteValues { get; set; }
public TransferToRouteResult(RouteValueDictionary routeValues)
: this(null, routeValues)
{
}
public TransferToRouteResult(string routeName, RouteValueDictionary routeValues)
{
this.RouteName = routeName ?? string.Empty;
this.RouteValues = routeValues ?? new RouteValueDictionary();
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var urlHelper = new UrlHelper(context.RequestContext);
var url = urlHelper.RouteUrl(this.RouteName, this.RouteValues);
var actualResult = new TransferResult(url);
actualResult.ExecuteResult(context);
}
}
And if you're using T4MVC (if not... do!) this extension might come in handy.
public static class ControllerExtensions
{
public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
{
return new TransferToRouteResult(result.GetRouteValueDictionary());
}
}
Using this little gem you can do
// in an action method
TransferToAction(MVC.Error.Index());
Edit: Updated to be compatible with ASP.NET MVC 3
Provided you are using IIS7 the following modification seems to work for ASP.NET MVC 3.
Thanks to #nitin and #andy for pointing out the original code didn't work.
Edit 4/11/2011: TempData breaks with Server.TransferRequest as of MVC 3 RTM
Modified the code below to throw an exception - but no other solution at this time.
Here's my modification based upon Markus's modifed version of Stan's original post. I added an additional constructor to take a Route Value dictionary - and renamed it MVCTransferResult to avoid confusion that it might just be a redirect.
I can now do the following for a redirect:
return new MVCTransferResult(new {controller = "home", action = "something" });
My modified class :
public class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url)
: base(url)
{
}
public MVCTransferResult(object routeValues):base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()), RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException("TempData won't work with Server.TransferRequest!");
}
httpContext.Server.TransferRequest(Url, true); // change to false to pass query string parameters if you have already processed them
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
You can use Server.TransferRequest on IIS7+ instead.
I found out recently that ASP.NET MVC doesn't support Server.Transfer() so I've created a stub method (inspired by Default.aspx.cs).
private void Transfer(string url)
{
// Create URI builder
var uriBuilder = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, Request.ApplicationPath);
// Add destination URI
uriBuilder.Path += url;
// Because UriBuilder escapes URI decode before passing as an argument
string path = Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
// Rewrite path
HttpContext.Current.RewritePath(path, false);
IHttpHandler httpHandler = new MvcHttpHandler();
// Process request
httpHandler.ProcessRequest(HttpContext.Current);
}
Couldn't you just create an instance of the controller you would like to redirect to, invoke the action method you want, then return the result of that? Something like:
HomeController controller = new HomeController();
return controller.Index();
Rather than simulate a server transfer, MVC is still capable of actually doing a Server.TransferRequest:
public ActionResult Whatever()
{
string url = //...
Request.RequestContext.HttpContext.Server.TransferRequest(url);
return Content("success");//Doesn't actually get returned
}
I wanted to re-route the current request to another controller/action, while keeping the execution path exactly the same as if that second controller/action was requested. In my case, Server.Request wouldn't work because I wanted to add more data. This is actually equivalent the current handler executing another HTTP GET/POST, then streaming the results to the client. I'm sure there will be better ways to achieve this, but here's what works for me:
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Public");
routeData.Values.Add("action", "ErrorInternal");
routeData.Values.Add("Exception", filterContext.Exception);
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var request = new RequestContext(context, routeData);
IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(filterContext.RequestContext, "Public");
controller.Execute(request);
Your guess is right: I put this code in
public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter
and I'm using it to display errors to developers, while it'll be using a regular redirect in production. Note that I didn't want to use ASP.NET session, database, or some other ways to pass exception data between requests.
Just instance the other controller and execute it's action method.
You could new up the other controller and invoke the action method returning the result. This will require you to place your view into the shared folder however.
I'm not sure if this is what you meant by duplicate but:
return new HomeController().Index();
Edit
Another option might be to create your own ControllerFactory, this way you can determine which controller to create.
Server.TransferRequest is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
What's more is that since you are responding to the original request, there is no need to tuck anything into TempData or other storage just for the sake of routing the request to the right place. Instead, you arrive at the controller action with the original request intact. You also can be rest assured that Google will approve of this approach as it happens entirely on the server side.
While you can do quite a bit from both IRouteConstraint and IRouteHandler, the most powerful extension point for routing is the RouteBase subclass. This class can be extended to provide both incoming routes and outgoing URL generation, which makes it a one stop shop for everything having to do with the URL and the action that URL executes.
So, to follow your second example, to get from / to /home/7, you simply need a route that adds the appropriate route values.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes directy to `/home/7`
routes.MapRoute(
name: "Home7",
url: "",
defaults: new { controller = "Home", action = "Index", version = 7 }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
But going back to your original example where you have a random page, it is more complex because the route parameters cannot change at runtime. So, it could be done with a RouteBase subclass as follows.
public class RandomHomePageRoute : RouteBase
{
private Random random = new Random();
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
result.Values["version"] = random.Next(10) + 1; // Picks a random number from 1 to 10
}
// If this isn't the home page route, this should return null
// which instructs routing to try the next route in the route table.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
action.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
// Route to the Home page URL
return new VirtualPathData(this, "");
}
return null;
}
}
Which can be registered in routing like:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes to /home/{version} where version is randomly from 1-10
routes.Add(new RandomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Note in the above example, it might make sense to also store a cookie recording the home page version the user came in on so when they return they receive the same home page version.
Note also that using this approach you can customize routing to take query string parameters into consideration (it completely ignores them by default) and route to an appropriate controller action accordingly.
Additional Examples
https://stackoverflow.com/a/31958586
https://stackoverflow.com/a/36774498
https://stackoverflow.com/a/36168395
Doesn't routing just take care of this scenario for you? i.e. for the scenario described above, you could just create a route handler that implemented this logic.
For anyone using expression-based routing, using only the TransferResult class above, here's a controller extension method that does the trick and preserves TempData. No need for TransferToRouteResult.
public static ActionResult TransferRequest<T>(this Controller controller, Expression<Action<T>> action)
where T : Controller
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
var url = LinkBuilder.BuildUrlFromExpression(controller.Request.RequestContext, RouteTable.Routes, action);
return new TransferResult(url);
}
Not an answer per se, but clearly the requirement would be not only for the actual navigation to "do" the equivalent functionality of Webforms Server.Transfer(), but also for all of this to be fully supported within unit testing.
Therefore the ServerTransferResult should "look" like a RedirectToRouteResult, and be as similar as possible in terms of the class hierarchy.
I'm thinking of doing this by looking at Reflector, and doing whatever RedirectToRouteResult class and also the various Controller base class methods do, and then "adding" the latter to the Controller via extension methods. Maybe these could be static methods within the same class, for ease/laziness of downloading?
If I get round to doing this I'll post it up, otherwise maybe somebody else might beat me to it!
I achieved this by harnessing the Html.RenderAction helper in a View:
#{
string action = ViewBag.ActionName;
string controller = ViewBag.ControllerName;
object routeValues = ViewBag.RouteValues;
Html.RenderAction(action, controller, routeValues);
}
And in my controller:
public ActionResult MyAction(....)
{
var routeValues = HttpContext.Request.RequestContext.RouteData.Values;
ViewBag.ActionName = "myaction";
ViewBag.ControllerName = "mycontroller";
ViewBag.RouteValues = routeValues;
return PartialView("_AjaxRedirect");
}