Controller searching for view instead of returning a different view method - asp.net-mvc

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});
}

Related

Custom Filter Attribute does not run on code call

I am using a Custom Action Filter to authorize users to Actions, some of which return an ActionResult while others return a JsonResult.
For every regular action system performs OK. But, now I have another requirement to implement where my design fails.
The View posts to:
[AuthorizationFilter(Entity = AuthEntity.MyItem, Permission = AuthPermission.Write)]
public JsonResult Edit(MyModel model)
where I check the user's authorization for Write operation. This check performs OK. But actually my Action just checks a condition and the redirects the Action to another Action in the Controller as follows:
[AuthorizationFilter(Entity = AuthEntity.MyItem, Permission = AuthPermission.Write)]
public JsonResult Edit(MyModel model)
{
if (model.Id == 0)
{
return Insert(model);
}
else
{
return Update(model);
}
}
Also the Update Action checks for a certain state which requires another authorization:
public JsonResult Update(MyModel model)
{
if (model.StatusId == (int)Shared.Enumerations.Status.Approved)
{
return UpdateRequiresApproval(model);
}
else
{
return UpdateRequiresNonApproval(model);
}
}
[AuthorizationFilter(Entity = AuthEntity.MyItem, Permission = AuthPermission.Approve)]
public JsonResult UpdateRequiresApproval(MyModel model)
The thing is, although I have a custom attribute filter defined on UpdateRequiresApproval action it does not run the filter (possibly) because it is being redirected by another action by means of a code call, but not from the View directly.
How can I make my filter run when code falls to the UpdateRequiresApproval action?
Regards.

View/Model data isn't refreshing/changing after post/postback, even though I'm using the PRG pattern

Update I have saved my problem a long time ago. The problem was that I was trying to call the view model on the wrong view method! I was calling the base view method (Document), instead of one of it's derived method (like NewDocument, PDFDocument, etc.) Thus it was only giving me the Documents data, which didn't change. I was looking and using the wrong view method all the time... Stephen, when you asked me
"Why do you create derived classes in a method but then return only the base class"
I couldn't answer the question at the time because I didn't even know myself, until I remember that originally, the method wasn't returning the base class. I only changed it so that it can work with the base view method, which was wrong in the first place!
That's what I get for only getting 3-4 hours of sleep in 3 days. Everything works right now. Thanks.
I'm having a hard time trying to figure out why the data in my view isn't changing after I do a post. Originally I was doing it via return View() and it worked, but since it was a partial view, the page didn't look great, so I was reading up and saw that it was better to do it by Post-Redirect-Get pattern (PRG) and to use an id value to retrieve the values instead of sending the entire model via Tempdata. I even used ModelState.Clear() and that didn't even work. When I debugged the code, the model only has the values from when I first called it.
Here's part of my Get controller:
NewDocument Get Controller
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// This doesn't work. The view keeps on showing the data from View(Services.CreateNewDocument()).
if (id != null)
{
return View(Services.GetdocumentViewModelData(DocEnum.Section.NEW_DOC_INDEX, (int)id));
}
// This works fine
return View(Services.CreateNewDocument());
}
And here's the post that calls the redirect:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
int? DocID = Services.AddingNewDocument(dvm);
// See, I even tried to clear it.
ModelState.Clear();
return base.RedirectToAction("NewDocument", new { id = DocID });
}
else
{
// Display errors in the modal
}
return base.RedirectToAction("NewDocument");
}
And here's the old way I did it:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
Services.AddingNewDocument(ref dvm);
dvm.NewRecordMode = DocEnum.Action.UPDATE;
// It worked, but only the partial view showed, and not the entire view.
return PartialView("_NewDocument", dvm);
}
else
{
// Display errors in the model
}
return base.RedirectToAction("NewDocument");
}
Could it be because I'm using a custom model binding?
My Custom Model Binding
public class BaseClassModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var modelType = bindingContext.ModelType;
var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ViewModel");
if (modelTypeValue == null)
throw new Exception("View does not contain the needed derived model type name");
var modelTypeName = modelTypeValue.AttemptedValue;
var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);
if (type == null)
{
throw new Exception(String.Format("Derived model type {0} not found", modelTypeName));
}
var instance = bindingContext.Model ?? base.CreateModel(controllerContext, bindingContext, type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
return base.BindModel(controllerContext, bindingContext);
}
}
EDIT: And here's the GetDocumentViewModelData code:
GetDocumentFromViewModelData
public static DocumentViewModel GetDocumentViewModelData(DocEnum.Section docType, int id)
{
switch (docType)
{
case DocEnum.Section.NEW_DOCUMENT_INDEX:
// NewDocumentTypeViewModel is a child to DocumentTypeViewModel
DocumentTypeViewModel nd = NewDocumentService.GetViewModelByID(id);
return nd;
case DocEnum.Section.PDF_DOCUMENT:
DocumentTypeViewModel pdfvm = PDFDocumentService.GetViewModelByID(id);
return pdfvm;
case DocEnum.Section.XLS_DOCUMENT:
DocumentTypeViewModel xlsvm = XLSDocumentService.GetViewModelByID(id);
return xlsvm;
}
return null;
}
Edit: Also adding the GetViewModelByID function
GetViewModelByID
public static DocumentTypeViewModel GetViewModelByID(int id)
{
docEntities db = new docEntities();
NewDocumentTypeViewModel vm = new NewDocumentTypeViewModel();
// Calls a stored procedure called Select_Documents_ByID(id) to get the note entry
// that was submitted.
List<Select_Documents_ByID_Result> prevNotes = db.Select_Documents_ByID(id).ToList();
StringBuilder sNotes = new StringBuilder();
foreach (var note in prevNotes)
{
sNotes.AppendFormat("{0} - {1}: {2}\n\n", note.CreatedDate.ToString("yyyy-MM-dd HH:mm"), note.username, note.Entry);
}
vm.PreviousNotes = sNotes.ToString();
return vm;
}
Edit: I did a direct creation of the view model inside the Get controller, and it's the same result. when i debugged the view itself, the values from the new view model don't show up. Instead, the values from the initial view model, View(Services.CreateNewDocument()), shows.
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// Right here I created the view model to test thing, but I'm getting the same results. Nothing has changed.
if (id != null)
{
var d = new NewDocumentTypeViewModel(1, "Help!");
// This property is from the base class, DocumentTypeViewModel
d.DocumentTitle = "Testing!";
return View(d);
// Inside the view itself, none of the values in the view model, including the one
// belonging to the base class. It still shows the initial values.
}
// This works fine
// Or maybe not...
return View(Services.CreateNewDocument());
}
Edit: I wanted to see if it was also doing the same thing for the initial call to the view return View(Services.CreateNewDocument()), and decided to change the value for documentTitle in the base class from New Document to a randomly-generated number, after the object has been created.
Here's the code for DocumentTypeViewModel's default constructor:
public DocumentTypeViewModel()
{
DocumentTitle = "New Document";
NewRecordMode = DocEnum.Action.ADD;
DocumentID = 0;
}
And here's the Services.CreateNewDocument() code where I change the DocumentTitle after the View Model has been created.
public DocumentTypeViewModel CreateNewDocument()
{
DocumentTypeViewModel dtvm = new DocumentTypeViewModel();
Random r = new Random();
dtvm.DocumentTitle = r.Next(5, Int32.MaxValue).ToString();
return dtvm;
}
Now in the View, when I call DocumentTitle:
<div class="label-text-group">
#Html.LabelFor(model => model.DocumentTitle)
#Html.EditorFor(model => model.DocumentTitle)
</div>
You would expect to see a randomly-generated number every time the View gets called. Nope, what you would see is "New Document". Weird.
It's seems that Services.GetDocumentViewModelData() is not exactly working correctly. It only carries the values created by the base class' constructor when a view is created, not any values that have been added or changed within GetDocumentViewModelData() itself. Why is that? What's going on? Please help anybody!
I have solved it. Look at the Update section on top. Thanks Stephen.

What Views can a Controller Action return?

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");
}

redirectToAction results in null model

I have 2 actions on a controller:
public class CalculatorsController : Controller
{
//
// GET: /Calculators/
public ActionResult Index()
{
return RedirectToAction("Accounting");
}
public ActionResult Accounting()
{
var combatants = Models.Persistence.InMemoryCombatantPersistence.GetCombatants();
Debug.Assert(combatants != null);
var bvm = new BalanceViewModel(combatants);
Debug.Assert(bvm!=null);
Debug.Assert(bvm.Combatants != null);
return View(bvm);
}
}
When the Index method is called, I get a null model coming out. When the Accounting method is called directly via it's url, I get a hydrated model.
This is less an answer than a workaround. I am not sure why you are getting a null model as it looks like it should work. In fact, I can confirm the behavior you are seeing when I try it out myself. [EDIT: I discovered a flaw in my initial test code that was causing my own null model. Now that that is corrected, my test works fine using RedirectToAction.] If there is a reason for it, I don't know it off the top of my head.
Now for the workaround...I assume that you are doing it this way since the default route sends all traffic to http://www.domain.com/Calculators to "Index". So why not create a new route like this:
routes.MapRoute(
"Accounting",
"Calculators/{action}/",
new { controller = "Calculators", action = "Accounting" }
);
This route specifies the default action to the Calculators controller will be "Accounting" instead of Index.
Your view for the Action Accounting expects a model. (the BalanceViewModel). The index action method does not have a instance of the BalanceViewModel.
There are a number of ways you can solve this. In your View (aspx page) you can check for nulls...
Or in the index action method, you create a new instance of a BalanceViewModel and store it in TempData, and then retrieve this in your view when your model is null.
Or in your action method, you could also call return View("Accounting", new BalanceViewModel()) instead of using redirect to action.
EDIT: Example Code -
If you want to share this functinality, create a private method like this:
public class CalculatorsController : Controller {
// GET: /Calculators/
public ActionResult Index() {
return View(GetBalanceViewModel());
}
public ActionResult Accounting() {
return View(GetBalanceViewModel());
}
private BalanceViewModel GetBalanceViewModel() {
var combatants = Models.Persistence.InMemoryCombatantPersistence.GetCombatants();
Debug.Assert(combatants != null);
var bvm = new BalanceViewModel(combatants);
Debug.Assert(bvm != null);
Debug.Assert(bvm.Combatants != null);
return bvm;
}
}
Have you seen this Question?

ASP.NET MVC - Reusing Action Behaviors

This question pertains primarily to good design.
Suppose I have a controller action like DeletePage that can be invoked in two separate views of the same controller. Assuming the delete logic is not contained in the action itself, but rather some conditional checks and the like that call the correct business logic, it doesn't make sense to duplicate the structure of the delete action when I can instead have a private method that returns an ActionResult which I call in both actions which can cause a delete. My question is where is the best place to place a reusable action method like this? Right now I'm just marking them private and sticking them in a region of the controller class, but perhaps an sealed inner class would make more sense for such a method- or somewhere else entirely.
Thoughts?
public ActionResult EditPage(int id, FormCollection formCollection)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
if (page.IsProtected)
return View("IllegalOperation");
if (formCollection["btnSave"] != null)
{
//...
}
else if (formCollection["btnDelete"] != null)
{
return DeletePage(page);
}
return RedirectToAction("Index");
}
public ActionResult DeletePage(int id)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
return DeletePage(page);
}
// Reusable Action
private RedirectToRouteResult DeletePage(Page page)
{
if(page != null && !page.IsProtected)
{
_pagesRepository.Delete(page);
_pagesRepository.Save();
FlashMessage(string.Format(PageForms.PageDeleted, page.Name), MessageType.Success);
return RedirectToAction("Index");
}
return RedirectToAction("Index");
}
I don't see why you need to make your reusable method an action method. Why not just a private method that returns void/bool/etc indicating the result of the save, and let your public action method return the RedirectToAction()? Effectively it's the same result, but I think it's a clearer approach.
public ActionResult DeletePage(int id)
{
var page = _pagesRepository.GetPage(id);
if (page == null)
return View("NotFound");
DeletePage(page);
return RedirectToAction("Index");
}
//reusable method
private void DeletePage(Page page)
{
//your same validation/save logic here
}
In the future you might consider moving this private DeletePage method into a separate service class that performs the validation and/or saving. Returning an an ActionResult would definitely not make sense in that case, so I think this example would be a more appropriate approach for your scenario.
In my opinion, your reusable code is an Action because it is returning an ActionResult. So your use is fine. The DeletePage(Page page) could potentially remain public.
I look forward to other opinions.
Personally I agree with Kurt. The concept of Deleting an unprotected page should be decoupled from what action the controller should perform. Secondly it's confusing from the code what should happen when the page is protected. In one action it redirects to the index, in the second it redirects to the "IllegalOperation" view. Personally I'd do something a little like...
public ActionResult DeletePage(int id) {
var page = _pagesRepository.GetPage(id);
if (!PageIsValidForDeletion(page)) {
string invalidPageView = FindViewForInvalidPage(page);
return View(invalidPageView);
}
DeletePage(page);
return RedirectToAction("Index");
}

Resources