ASP.NET MVC - Reusing Action Behaviors - asp.net-mvc

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

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.

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 make ASP.NET MVC Action return different formats?

I don't know if this is the right way to approach something, but I'm hoping it is. The example below is a heavy controller and is absolutely the wrong approach, but it get's the idea of what I'm looking for across.
public class PeopleController : Controller
{
public ActionResult List(string? api)
{
MyViewModel Model = new MyViewModel();
if (api == "json") {
// I'd like to return the Model as JSON
} else if (api == "XML") {
// I'd like to return the Model as XML
} else {
return View(Model);
}
}
}
Now what I need to be able to do is return the Model to the View if it's being requested like this:
http://example.com/People/List
But I'd like it to output JSON if it's requested like this:
http://example.com/People/List/?api=json
Or output XML if it's requested like this:
http://example.com/People/List/?api=xml
Is this just plain wrong? If not, what is the best approach to achieve this?
I was thinking of achieving it with a Custom MultiPurposeResult that could do all the filtering for me and then return it as this
public class PeopleController : Controller
{
public MultiPurposeResult List(string? api)
{
MyViewModel Model = new MyViewModel();
return MultiPurpose(Model); }
}
}
Agree with #Matt. Don't explicitly ask for the response type, infer it from the contentType in the request, which is more RESTful.
For example, created a basic enum type to encapsulate the response types you want:
public enum RestfulResultType
{
Json,
Html,
Xml
}
Then create a custom model binder than sets this property in your action, depending on the content type.
Then your controller could look like this:
public ActionResult List(RestfulResultType resultType)
{
var data = repo.GetSomeData();
switch (resultType)
{
case RestfulResultType.Json:
return Json(data);
case RestfulResultType.Xml:
return XmlResult(data); // MvcContrib
case RestfulResultType.Html:
return View(data);
}
}
If you need any more customization than the regular helpers provide, then create custom ActionResult's.
You can leave the return type as ActionResult - that's the point, so that the controller can return different formats.
ResfulResultTypeModelBinder.cs:
public class ResfulResultTypeModelBinder: IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.HttpContext.Request.ContentType == "application/json")
return RestfulResultType.Json;
// other formats, etc.
}
}
Global.asax:
ModelBinders.Binders.Add(typeof(RestfulResultType), new RestfulResultTypeModelBinder());
You can create a custom MultiPurposeResult but I personally would lose the string? api from the method signature, instead have the MultiPurpose look for the presence of Request["format"] and then make the determination of what format to possible output the results in. Since the format doesn't nessecarily have anything to do with the ActionResult but more the format of the response.

Asp.Net MVC3 Redirect

I have an action like shown below. In GetAvailableBookList, I get the list and if there is not any available book redirect to a message page. But in action part code continues to execute and gets an exception and I find myself in error page.
I don't want to use return RedirectToAction or something like that because there are a lot of places where we use this redirect logic in our application.
public ActionResult ActionName()
{
List<BookType> bookList = GetAvailableBookList();
// some code
return View("RelatedView");
}
private List<BookType> GetAvailableBookList()
{
....
list = GetList();
if(list.Count == 0)
{
System.Web.HttpContext.Current.Response.Redirect(messagePageUrl, true);
}
else return list;
}
Unfortunately, Response.Redirect() isn't really friendly with ASP.NET MVC. My rule of thumb is if it comes from HttpContext I don't want to touch it in the controller (of course there are many exceptions to that rule) -- especially since it improves testability.
My suggestion is to use RedirectToAction, but since you don't want to repeat code you can do it in such a way that you don't have to repeat code (although in this case I don't see a problem with repeating code).
public ActionResult LoadBookListAndContinue(
Func<List<BookType>, ActionResult> continuation)
{
var list = LoadBooklist();
if(list.Any())
{
return action(continuation);
}
return new RedirectResult(messagePageUrl);
}
// in your controller
public ActionResult ActionName()
{
return LoadBookListAndContinue(
list => {
// some code
return View("RelatedView");
});
}
Is it pretty? No, but it works better than the Redirect exception.
Use
return RedirectToAction("NoListAvailable");
if you have a specific action you would like to execute. The NoListAvailable action can return a view indicating the problem.
Alternatively, you could return the view directly
return View("NoListAvailable");
The exception you are getting is probably ThreadAbortException and this is something you cannot avoid unless you allow the thread to continue (2nd argument in Response.Redirect).
On a side note your current solution is generally flawed. You should use RedirectToAction in each action when your method returns an empty list.
Throwing a specific exception and redirect where you catch it may be solution
Try to write
System.Web.HttpContext.Current.Response.Redirect(messagePageUrl, false);

Two step authentication in MVC?

We have an MVC app which has a custom forms authentication view/controller. The controller will verify things and then do a FormsAuthentication.RedirectFromLoginPage call.
At this point in the Global.asax we'll receive a Application_OnAuthenticateRequest call from where we'll get their Context.User information and make another call to gather information relevant to this account which we then store in their Context.User & System.Threading.Thread.CurrentPrincipal. We also do a little caching of this information since in our system retrieving what we need is expensive which leads to cache invalidation & re-retrieval of this information.
It seems a bit odd at this point that we've got these separated into separate calls. I'm almost wondering if the Login controller shouldn't be gathering the details as part of its authentication check and storing them. Then the Application_OnAuthenticateRequest can only worry about if the cache needs to be invalidated and the users details re-retrieved.
Or maybe there is some other way of handling this I don't even know about..?
You can do what you want in MVC by leveraging RedirectToRouteResult and a custom cache updating ActionFilter. This is called the PRG (Post-Redirect-Get) pattern. You are actually already doing this, but it gets a little confused, because what you are doing is a cross between the classic ASP.NET way of doing things and the MVC way of doing things. There's nothing wrong with your initial approach (provided it is working correctly), but to do the same sort of thing and have more control and understanding of how it works in the scheme of things you could do something like:
public class AuthenticationController :Controller
{
[HttpPost]
public RedirectToRouteResult Login(string username, string password)
{
//authenticate user
//store authentication info in TempData like
bool authenticated = true|false; // do your testing
if(authenticated)
{
TempData["MustUpdateCache"] = true | false;
return RedirectToAction("LoginSuccess", new{userId = membershipUser.UserId});
}
else
{
TempData["MustUpdateCache"] = true | false;
return RedirectToAction("Login");
}
}
[HttpGet, UpdateCache]
public ActionResult LoginSuccess(Guid userId, string url)
{
HttpContext.User = LoadUser(userId);
return View();
}
[HttpGet, UpdateCache]
public ViewResult Login()
{
return View();
}
}
public class UpdateCacheAttribute:ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var tempData = filterContext.Controller.TempData;
if (tempData.ContainsKey("MustUpdateCache") && (bool)tempData["MustUpdateCache"])
{
UpdateCache(filterContext);
}
}
void UpdateCache(ControllerContext controllerContext)
{
//update your cache here
}
}

Resources