Form Logic and Forms for ASP MVC Workflow

I am creating a workflow tool that will be used on our corporate intranet. Users authenticate using Windows authentication, and I created a custom RoleProvider that maps each user to a couple of roles.

One role indicates their seniority (guest, user, senior user, manager, etc.), and the other indicates their role / department (analytics, development, testing, etc.). Users in Analytics can create a query, which then translates the chain into "Development", etc.:

Models

public class Request { public int ID { get; set; } ... public virtual ICollection<History> History { get; set; } ... } public class History { public int ID { get; set; } ... public virtual Request Request { get; set; } public Status Status { get; set; } ... } 

In the controller, I have a Create () method that will create a request header record and the first history element:

Query controller

 public class RequestController : BaseController { [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create (RequestViewModel rvm) { Request request = rvm.Request if(ModelState.IsValid) { ... History history = new History { Request = request, Status = Status.RequestCreated, ... }; db.RequestHistories.Add(history); db.Requests.Add(request); ... } } } 

Each subsequent stage of the request must be processed by different users in the chain. A small subset of the process:

  • User makes a request [Analytics, User]
  • Manager resolves request [Analytics, Manager]
  • Developer Processes Request [Development, User]

Currently, I have one CreateHistory () method that processes every step of the process. The state of the new History element is deduced from the view:

 // GET: Requests/CreateHistory public ActionResult CreateHistory(Status status) { History history = new History(); history.Status = status; return View(history); } // POST: Requests/CreateHistory [HttpPost] [ValidateAntiForgeryToken] public ActionResult CreateHistory(int id, History history) { if(ModelState.IsValid) { history.Request = db.Requests.Find(id); ... db.RequestHistories.Add(history); } } 

The CreateHistory View will display a different partial form depending on the state. My intention was that I could use one common CreateHistory method for each step of the process, using Status as a reference to determine which partial view to display.

Now the problem is rendering and limiting the available actions in the view. My CreateHistory View gets bloated with If statements to determine if actions are available depending on the current current state:

 @* Available user actions *@ <ul class="dropdown-menu" role="menu"> @* Analyst has option to withdraw a request *@ <li>@Html.ActionLink("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null)</li> @* Request manager approval if not already received *@ <li>...</li> @* If user is in Development and the Request is authorised by Analytics Manager *@ <li>...</li> ... </ul> 

Creating the right actions at the right time is the easy part, but it seems like a clumsy approach, and I'm not sure how I will manage permissions this way. So my question is:

Should I create a separate method for each step of the process in the RequestController, even if this leads to many very similar methods?

Example:

 public ActionResult RequestApproval(int id) { ... } [MyAuthoriseAttribute(Roles = "Analytics, User")] [HttpPost] [ValidateAntiForgeryToken] public ActionResult RequestApproval(int id, History history) { ... } public ActionResult Approve (int id) { ... } [MyAuthoriseAttribute(Roles = "Analytics, Manager")] [HttpPost] [ValidateAntiForgeryToken] public ActionResult Approve (int id, History history) { ... } 

If so, how do I handle the display of the corresponding buttons in the view? I want a set of valid actions to be displayed as controls.

Sorry for the long post, any help would be greatly appreciated.

+6
source share
5 answers

First of all, if you have a lot of logic encapsulated in logical operations, I highly recommend using the this pattern of characteristics and this should start you well. It is very reusable and provides great maintainability when changing existing logic or you need to add new logic. Examine composite specifications that pinpoint what can be done, for example. If the user is a manager and the request is not confirmed.

Now, regarding your problem, in your opinion - although when I came across the same problem in the past, I took a similar approach to ChrisDixon . It was simple and easy to work, but, looking back at the application, now I find it tedious, as it focuses on if statements. The approach I would take now is to create custom action links or custom controls that, when possible, take permission in context. I started writing code for this, but at the end I realized that this should be a common problem, and therefore found something much better than I intended to write for this answer. Although it targets MVC3, logic and purpose should be maintained.

Below are the fragments in case of deleting the article. :)

This is an extension method that validates the controller for an Authorized attribute. In the foreach you can check for your own custom attribute and enable it.

 public static class ActionExtensions { public static bool ActionAuthorized(this HtmlHelper htmlHelper, string actionName, string controllerName) { ControllerBase controllerBase = string.IsNullOrEmpty(controllerName) ? htmlHelper.ViewContext.Controller : htmlHelper.GetControllerByName(controllerName); ControllerContext controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerBase); ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(controllerContext.Controller.GetType()); ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName); if (actionDescriptor == null) return false; FilterInfo filters = new FilterInfo(FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor)); AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor); foreach (IAuthorizationFilter authorizationFilter in filters.AuthorizationFilters) { authorizationFilter.OnAuthorization(authorizationContext); if (authorizationContext.Result != null) return false; } return true; } } 

This is a helper method for getting a ControllerBase object, which is used in the above snippet to poll action filters.

 internal static class Helpers { public static ControllerBase GetControllerByName(this HtmlHelper htmlHelper, string controllerName) { IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory(); IController controller = factory.CreateController(htmlHelper.ViewContext.RequestContext, controllerName); if (controller == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "The IControllerFactory '{0}' did not return a controller for the name '{1}'.", factory.GetType(), controllerName)); } return (ControllerBase)controller; } } 

This is a custom Html Helper that generates an action link if authorization passes. I changed it from the original article to remove the link if it is not authorized.

 public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) { if (htmlHelper.ActionAuthorized(actionName, controllerName)) { return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes); } else { return MvcHtmlString.Empty; } } 

Call it what ActionLink calls normally.

 @Html.ActionLinkAuthorized("Withdraw", "CreateHistory", new { id = Model.Change.ID, status = Status.Withdrawn }, null) 
+1
source

When encoding in MVC (or, well, in any language), I try to keep all or most of my logical instructions away from my views.

I would save your logical processing in ViewModels, therefore:

 public bool IsAccessibleToManager { get; set; } 

Then, in your opinion, just use this variable as @if(Model.IsAccessibleToManager) {} .

Then it is populated in your controller and can be installed, but you can fit, potentially in the role logic class, which stores all this in one place.

As for the methods in your controller, save them the same method and do the logical processing inside the method itself. It all depends entirely on your structure and data repositories, but I would keep as much logical processing at the repository level as the same thing in every place where you get / install this data.

Usually you have attribute tags to prevent these methods for certain roles, but with your script you could do it this way ...

 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Approve (int id, History history) { try { // The logic processing will be done inside ApproveRecord and match up against Analytics or Manager roles. _historyRepository.ApproveRecord(history, Roles.GetRolesForUser(yourUser)); } catch(Exception ex) { // Could make your own Exceptions here for the user not being authorised for the action. } } 
+1
source

How to create different views for each type of role, and then return the corresponding view from one action?

 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Approve (int id, History history) { // Some pseudo-logic here: switch(roles) { case Manager: case User: { return View("ManagerUser"); } case Manager: case Analyst: { return View("ManagerAnalyst"); } } } 

Of course, for this approach you will need to create a view for different combinations of roles, but at least you can display the appropriate view code if the user interface logic does not clutter the views.

+1
source

I suggest you use a provider to create a list of available actions for the user.

First, I would define an AwailableAction enum, than describe what action your users might have. You may already have one.

Then you can define the IAvailableActionFactory interface and implement it using your logic:

 public interface IAvailableActionProvider { ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History/*, etc*/) // Provide parameters that need to define actions. } public class AvailableActionProvider : IAvailableActionProvider { ReadOnlyCollection<AwailableAction> GetAvailableActions(User, Request, History) { // You logic goes here. } } 

Inside this provider, the same logic that you are currently implementing in the view will be used. This approach will keep the view clean and provide verifiable logic. Optionally, within the provider's, you can use different strategies for different users and make the implementation even more untied.

Then, in the controller, you determine the dependency on this provider and enable it directly through the instantioate container, if you are not already using the container.

 public class RequestController : BaseController { private readonly IAvailableActionProvider _actionProvider; public RequestController(IAvailableActionProvider actionProvider) { _actionProvider = actionProvider; } public RequestController() : this(new AvailableActionProvider()) { } ... } 

Then, in your action, use the provider to get the available actions, you can either create a new view model, or contain actions, or just put it in the ViewBag :

 // GET: Requests/CreateHistory public ActionResult CreateHistory(Status status) { History history = new History(); history.Status = status; ViewBag.AvailableActions = _actionProvider.GetAvailableActions(User, Request, history); return View(history); } 

And finally, you can create an action list based on the elements in the ViewBag .

Hope this helps. Let me know if you have any questions about this.

+1
source

I would advise using claims along with roles. If the role requires access to the resource, I will give them a request for the resource, that is, actionResult. If their role matches the controller, for simplicity I will check to see if they have a claim to the resource. I use roles at the controller level, so if a guest or some other account needs anonymous access, I can just add the attribute, but most often I had to put it in the right controller.

Here is a sample code.

 <Authorize(Roles:="Administrator, Guest")> Public Class GuestController Inherits Controller <ClaimsAuthorize("GuestClaim")> Public Function GetCustomers() As ActionResult Dim guestClaim As Integer = UserManager.GetClaims(User.Identity.GetUserId()).Where(Function(f) f.Type = "GuestClaim").Select(Function(t) t.Value).FirstOrDefault() Dim list = _customerService.GetCustomers(guestClaim) Return Json(list, JsonRequestBehavior.AllowGet) End Function End Class 
0
source

Source: https://habr.com/ru/post/1015609/


All Articles