What Views can a Controller Action return? - asp.net-mvc

In ASP.NET MVC 5, does every Controller Action have to return a View with the same name as the Controller?
Here's my project. Have a webpage which contains a button to upload an image to a database. When the webpage is loaded, I want it to display a list of all the images that have already been uploaded. So, the Index (default) Action for this Controller loads the images from the database, and returns the Index View, which in turn displays the list of images:
public ActionResult Index()
{
// Load the images from the database
var images = GetImages();
return View(images);
}
On that same webpage, there is a button which allows the user to upload an image to the database. That button calls the Upload Action, which uploads the file based upon the "file" and "folder" arguments that are passed, and then finally returns the Index View again:
[HttpPost]
public ActionResult Upload(HttpPostedFileBase file, string folder)
{
// Upload the file from the specified folder
// ...
// ...
// ...
return Index();
}
However, when a user clicks on this upload button, the following error message is displayed:
The view 'Upload' or its master was not found or no view engine supports the searched locations
But I am not trying to render a View called "Upload" - I am trying to render the view called "Index", which is why I have the line return Index();.
Any help on where I'm going wrong?

Answer
Although Vitaliy and Nathan A provided adequate answers, I wanted to explain why your initial approach doesn't work because it's a great question and doesn't seem to make sense.
To get our answer we have to look at the ASP.NET MVC source code.
Before we get to that let's walk through your code.
The user visits (or POSTS to) /Controller/Upload
We do some logic and then return Index()
Index() is a method that returns its own view with its own model
MVC fails to find 'Upload' view and throws an exception
What went wrong?
Firstly know that Index() is being called and returned successfully. The model object is also being passed to the view (if one is found).
When you return Index(), it is returning View() which is an inherited method from the Controller class which returns a ViewResult.
A ViewResult inherits from ViewResultBase.
When a ViewResult is being returned it calls ExecuteResult().
Taking a look at the source code for ExecuteResult():
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(this.ViewName))
{
this.ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult viewEngineResult = null;
if (this.View == null)
{
viewEngineResult = this.FindView(context);
this.View = viewEngineResult.View;
}
TextWriter output = context.HttpContext.Response.Output;
ViewContext viewContext = new ViewContext(context, this.View, this.ViewData, this.TempData, output);
this.View.Render(viewContext, output);
if (viewEngineResult != null)
{
viewEngineResult.ViewEngine.ReleaseView(context, this.View);
}
}
The key here is context.RouteData.GetRequiredString("action"). This code gets the action name so that a view can be found and rendered. Note that it is using a ControllerContext.
Because of this, your action is actually set when the Upload() method is first called. If you step through your Index() method you will see that calling context.RouteData.GetRequiredString("action") will return the string "Upload".
This is because, within the context of the user request, the action is in fact Upload (that's the page they requested).
Fun fact
If you return Index() and that method happens to alter the ViewBag (ViewData) then the ViewData will be altered regardless of what is rendered.
If your Upload() does this:
ViewBag.Test = "Upload method";
And you return Index() and your Index() does this:
ViewBag.Test = "Index method";
Then the value of Test will be "Index method".

Look up the documentation on the View method. It has several arguments you can provide, one of them being a string of the name of the view, but you always use the View() method if you want to return a view.
However, if you don't want to use the default View name (being the name of the action method), simply use a string to specify a new name like so:
public ActionResult Upload(HttpPostedFileBase file, string folder)
{
return View("Index");
}

you can do either:
return RedirectToAction("Index");
or:
return View("Index");

Just a side-note, you're not constrained to show views from the View folder for the controller name. You can do
public ActionResult something()
{
return View("../OtherView/somethingElse");
}

Related

Return action to action in asp.net mvc

Why my return to InsertUpdate() from Insert action not working when calling insert action using jQuery load function.It shows following error:
The partial view 'Insert' was not found or no view engine supports the
searched locations. The following locations were searched:
~/Views/AppClient/Insert.aspx
~/Views/AppClient/Insert.ascx
~/Views/Shared/Insert.aspx
~/Views/Shared/Insert.ascx
~/Views/AppClient/Insert.cshtml
~/Views/AppClient/Insert.vbhtml
~/Views/Shared/Insert.cshtml
~/Views/Shared/Insert.vbhtml
public ActionResult Insert()
{
return InsertUpdate();
}
private ActionResult InsertUpdate()
{
return PartialView();
}
Even though the actual action invoked is InsertUpdate() the context in which it is invoked is still the Insert() method (mainly because action in your routing values would still be insert).
Hence, when you don't specify a view to be rendered in your call to PartialView(), the name of the view is based on the current routing data.
Try this instead:
return PartialView("InsertUpdate");
Or manually change the routing data upon invoking InsertUpdate():
public ActionResult Insert()
{
this.RouteData.Values["action"] = "InsertUpdate";
return InsertUpdate();
}

OnActionExecuting fires multiple times

I'm not sure if this is the correct way to go about the problem I need to solve... however in an OnActionExecuting action filter that I have created, I set a cookie with various values. One of these values is used to determine whether the user is visiting the website for the very first time. If they are a new visitor then I set the ViewBag with some data so that I can display this within my view.
The problem I have is that in some of my controller actions I perform a RedirectToAction. The result is OnActionExecuting is fired twice, once for the original action and then a second time when it fires the new action.
<HttpGet()>
Function Index(ByVal PageID As String) As ActionResult
Dim wo As WebPage = Nothing
Try
wp = WebPages.GetWebPage(PageID)
Catch sqlex As SqlException
Throw
Catch ex As Exception
Return RedirectToAction("Index", New With {.PageID = "Home"})
End If
End Try
Return View("WebPage", wp)
End Function
This is a typical example. I have a data driven website that gets a webpage from the database based on the PageID specified. If the page cannot be found in the database I redirect the user to the home page.
Is it possible to prevent the double firing in anyway or is there a better way to set a cookie? The action filter is used on multiple controllers.
Had the same issue. Resolved by overriding property AllowMultiple:
public override bool AllowMultiple { get { return false; } }
public override void OnActionExecuting(HttpActionContext actionContext)
{
//your logic here
base.OnActionExecuting(actionContext);
}
You can save some flag value into TempData collection of controller on first executing and if this value presented, skip filter logic:
if (filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] == null)
{
filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] = true;
}
You could return the actual action instead of redirecting to the new action. That way, you dont cause an http-request, thereby not triggering the onactionexecuting (i believe)
Old question, but I just dealt with this so I thought I'd throw in my answer. After some investigating I disovered this was only happening on endpoints that returned a view (i.e. return View()). The only endpoints that had multiple OnActionExecuting fired were HTML views that were composed of partial views (i.e. return PartialView(...)), so a single request was "executing" multiple times.
I was applying my ActionFilterAttribute globally to all endpoints, which was working correctly on all other endpoints except for the view endpoints I just described. The solution was to create an additional attribute applied conditionally to the partial view endpoints.
// Used specifically to ignore the GlobalFilterAttribute filter on an endpoint
public class IgnoreGlobalFilterAttribute : Attribute { }
public class GlobalFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Does not apply to endpoints decorated with Ignore attribute
if (!filterContext.ActionDescriptor.GetCustomAttributes(typeof(IgnoreGlobalFilterAttribute), false).Any())
{
// ... attribute logic here
}
}
}
And then on my partial view endpoints
[HttpGet]
[AllowAnonymous]
[IgnoreGlobalFilter] //HERE this keeps the attribute from firing again
public ActionResult GetPartialView()
{
// partial view logic
return PartialView();
}

Controller searching for view instead of returning a different view method

I have two controller actions outlined below:
public ViewResult TitleStorylines(int id)
{
var vm = Service.Get(id);
vm.IsEditable = User.HasPermission(SecurityPermissionType.ManageStorylines);
return View(vm);
}
public ViewResult TitleStorylinesCreate(TitleStorylineModel model)
{
var created = Service.Create(model);
return TitleStorylines(created.TitleId);
}
I only have one view in my project, called TitleStorylines, which the first controller action handles fine. But when I call the second method, it gives me an error saying that it can't find the view called TitleStorylinesCreate even though I'm explicitly calling the previous method. What gives?
Did you try ?
return View("TitleStorylines",created.TitleId);
EDIT: Based on your update : I guess you are posting your form back the TitleStorylinesCreate. So probably after saving, dont you want to redirect the user back to the Get action of same ?
[HttpPost]
public ViewResult TitleStorylinesCreate(TitleStorylineModel model)
{
var created = Service.Create(model);
return RedirectToAction("TitleStorylines",new { #id=created.TitleId});
}
In the above example we are doing the PRG pattern. Post -> Redirect -> Get
After saving, we are redirecting them back to the first method. It will be a HTTP GET method.
public ActionResult TitleStorylinesCreate(TitleStorylineModel model)
{
var created = Service.Create(model);
return RedirectToAction("TitleStorylines",new { #id=created.TitleId});
}

How to pass thw Viewdata to all the views in my controller?

i have a dropdown list which select a value
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Screenname(FormCollection collection)
{
Viewdata["screenname"] = collection[0];
return RedirectToAction("Index", new { ScreenName = ViewData["screenname"] });
}
then i want to access this ViewData in other actions like this
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection, string screenname)
{
try
{
/// thats my dataobject which creates
DataObj.SaveData(Guid.Empty, collection, screenname);
return RedirectToAction("Index", new { ScreenName = ViewData["screenname"] });
}
catch
{
return View("Error");
}
}
where index looks like this ...
public ActionResult Index(string ScreenName)
{
///thats my list
GetTable = new GetDataTable(ScreenName);
return View(GetTable);
}
First when i select the value and index gets executed properly.... but when i try to access the viewdata again it doesn't contain the value so anybody if please can help ...
or alternate method to save and retrieve data .
The ViewData object is specific for the particular action that is executing. To pass data between actions, use TempData. more on the difference between the two on MSDN.
You can also directly write to the session state through the Controller.Session property.
This has actually been covered quite often here. The solution for now is to use TempData to save the data you need before you use RedirectToAction().
If you do a search for "RedirectToAction" you'll find a number of posts covering this topic, such as this one.
The next official release of the framework will fix this.
I used a view to take the data from the user and then saved it to a static variable and then used this variable to pass the data to all the other views .
Thanks anyways

Can't pass viewmodel to new action by RedirectToAction

I have a form, when user submit the form, I want to direct the user the new view to display the submitted result(transfer viewmode data to display view).
public class HomeController : Controller
{
private MyViewModel _vm;
.....
// POST: /Home/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(MyViewModel vm){
//.....
//set up vm to temp data _vm
_vm = vm;
return RedirectToAction("DisplayData");
}
// GET: /Home/DisplayData
public ActionResult DisplayData()
{
//get temp data for display
return View(_vm);
}
}
When I posted the form, I can create vm and put it to temp data place _vm. But this _vm can be sent to another action DisplayData, it's null in action DisplayData(). It seems that when redirect action even in same controller, _vm is lost although it is Controller var, not action method var.
How to resolve this problem?
It creates a new instance of the controller as it is a new request therefore as you have found it will be null.
You could use TempData to store the vm, TempData persists the data for 1 request only
Good explanation here
One good way is to call
return DisplayData(_vm)
instead of
RedirectToAction("DisplayData")
DisplayData should accept a model anyway.

Resources