We're currently rewriting our organizations ASP.NET MVC application which has been written twice already. (Once MVC1, once MVC2). (Thank god it wasn't production ready and too mature back then).
This time, anyhow, it's going to be the real deal because we'll be implementing more and more features as time passes and the testruns with MVC1 and MVC2 showed that we're ready to upscale.
Until now we were using Controller and Action authorization with AuthorizeAttribute's.
But that won't do it any longer because our views are supposed to show different results based on the logged in user.
Use Case: Let's say you're a major of a city and you login to a federal managed software and you can only access and edit the citizens in your city.
Where you are allowed to access those citizens via an entry in a specialized MajorHasRightsForCity table containing a MajorId and a CityId.
What I thought of is something like this:
Public ViewResult Edit(int cityId) {
if(Access.UserCanEditCity(currentUser, cityId) {
var currentCity = Db.Cities.Single(c => c.id == cityId);
Return View(currentCity);
} else {
TempData["ErrorMessage"] = "Yo are not awesome enough to edit that shizzle!"
Return View();
}
The static class Access would do all kinds of checks and return either true or false from it's methods.
This implies that I would need to change and edit all of my controllers every time I change something. (Which would be a pain, because all unit tests would need to be adjusted every time something changes..)
Is doing something like that even allowed?
That's pretty much what I'd do for a decent sized application.
I would return a generic error view return View("Error"); if the user doesn't have any access so you don't need to handle the an error message on every View.
Conceptually, this (the Controller) is where the logic determining what View to return should lie. It stops business logic bleeding into the View.
You could abstract out certain role-dependant parts of the page to partial views to keep the clutter down:
<% if (User.IsInRole("Admin")) { %>
Html.RenderPartial("AdminPanel");
<% } %>
Have you thought about writing your own Action Filter Attribute?
That way you could just decorate your controller with the attribute and save you a lot of copying and pasting, plus if it ever needs to be changed then its only done in one place.
If you get the source code for MVC from Codeplex you can see how the Authorize attribute is done and modify that.
Related
In MVC 5 I am attempting to use the controller to render a partial view only if the (Windows Authenticated) user belongs to one or more of a list of Active Directory groups. There are over 30 distinct groups I need to account for, so the "hello world" examples don't fit my needs. After playing scavenger hunt on the web, I managed to collect this much. No compile or runtime errors, but the content is showing for all users rather than the specific users. So the desired outcome is not yet achieved.
While I can achieve the desired outcome using if-then logic in the view, it creates a lot of unnecessary duplication and encourages spaghettification. So I'm trying to do this in the controller.
Summary of Desired Outcome:
When the user loads the viewpage, the partial view should only render if the Windows Authenticated user belongs to one or more of a list of groups defined in the controller action. If the user is not authorized, then the partial view is not included.
Controller Block:
[ChildActionOnly]
[Authorize(Roles="Domain\\GroupA,Domain\\GroupB")]
public ActionResult MonitorCSU()
{
return PartialView("MonitorCSU");
}
View Block:
<div class="rowWithCols3">
#Html.Partial("MonitorCSU")
Unsuccessful Iterations:
In the controller block I tried (unsuccessfully) to use an if-then block, the else case being another partial view with no content.
[ChildActionOnly]
public ActionResult MonitorCSU()
{
if (User.IsInRole("Domain\GroupA")) {
return PartialView("_MonitorCSU");
}
else
{
return PartialView("_Unauthorized");
}
}
In Razor, I tried using HTML.Action but when I tried run the page the browser hung in an infinite loop.
#Html.Partial() returns a partial view without calling a controller method. In order to call your controller method, you need to use
#Html.Action("MonitorCSU")
or
#{ Html.RenderAction("MonitorCSU") }
Note this assumes that the MonitorCSU() method is in the same controller as the method that generates the main view (other wise you also need to include a parameter for the controller name)
Refer documentation
While you've found a solution, you're going to have other problems with it. I would suggest a different approach, which is to use EditorTemplates and create a separate model for the html you want to render. Then, at runtime you would check whether the user is in the groups you specify, and if they are, you create an instance of the model, and if they are not you leave the model null. In this way, when the view is rendered with EditorFor(), it will ignore and not render the template for users who do not have access.
Thanks to #Stephen Muecke and and a commenter whose entry has mysteriously vanished, I have the missing pieces.
I was able to test this code with several real users and verified the desired behavior happens consistently.
Controller Block:
Main difference: take out authorization and use an if-then block send one of two partial views.
[ChildActionOnly]
public ActionResult MonitorCSU()
{
if (User.IsInRole("DOMAIN\\GroupA"))
{
return PartialView("MonitorCSU");
}
else
{
return PartialView("Unauthorized");
// this is an empty page
}
}
View Block:
The key difference is using HTML.Action
<div class="rowWithCols3">
#Html.Action("MonitorCSU")
In my application exist a little role system. "Admin" and "normal" users. In my application I display data in tables. An admin must have the possibility to create, edit and delete these data, but the "normal" user must not.
So I thought displaying based on the user different Views would be a good idea. But I am struggleing currently with the implementation.
Controller:
public ActionResult Index()
{
if(model.IsAdmin)
{
... // get data
}
else
{
... // get data, but less, or different
}
return (model.IsAdmin) ? this.View("AdminView") : this.View("NormalView");
}
It could look like this.
Is it a good idea to create seperate views on one controller?
Should I work with partial views instead and do the logic inside my view to decide which one I want to display? Doesn't seem right.
What about the folder structure and file naming? Would be nice if they would both match to MyController/Action/Index, instead of .../Index and .../Index2
Thank you and best regards
1.
Most certain this is not a good practice.
You should try to create different Controllers for each Role. Or you could user Areas to point to admin.
If you just load a different view, the guest user could still introduce in the url bar something like "/admin/deleteSmth/1", and do admin actions.
The best it would be if you would implement different controllers or areas.
You should keep the logic far away from views. View are made only to render data. That should be all. All the logic should be made higher than the controller, if you have a business logic layer.
http://www.codeproject.com/Tips/601504/Using-areas-in-ASP-NET-MVC-to-organize-a-project . Areas, use them.
One way of doing this could be
public ActionResult Index()
{
if(model.IsAdmin)
{
... // get data
return view("AdminView")
}
else
{
... // get data, but less, or different
return view("NormalView")
}
//no need to perform extra checks
}
I have a registration page in my application. It has 3 states and 1 error state(If any error comes):
Fill Basic Information
Select Package
Say Thanks
Error
Now I want to use state pattern here. First I created a console application which is OK. Now I want to implement this logic in my MVC application but I am confused about the structure. I mean how many views, models and controller I need and where to place my logic.
1 controller: RegistrationController
6 action methods:
GET+POST for Index (fill in basic info)
GET+POST for Package
GET for Thank you
GET for Error
This is rough code to make your mind going:
public class RegistrationController : Controller
{
public ActionResult Index()
{
RegistrationState model = RegistrationState.Init();
// just display the "Fill Basic Info" form
return View(model);
}
[HttpPost]
public ActionResult Index(RegistrationState data)
{
// process data and redirect to next step
this.TempData["RegState"] = data;
if (!this.ModelState.IsValid || data.State == State.Error)
{
// error should handle provided state and empty one as well
return RedirectToAction("Error");
}
return RedirectToAction("Package");
}
public ActionResult Package()
{
RegistrationState data = this.TempData["RegState"] as RegistrationState;
if (data == null)
{
return RedirectToAction("Error");
}
// get packages and display them
IList<Package> model = this.repository.GetPackages();
return View(new Tuple.Create(data, model));
}
[HttpPost]
public ActionResult Package(RegistrationState data)
{
// process data blah blah blah
}
// and so on and so forth
....
}
As you can see you still have to write some MVC-related code to act upon state changes. In my example everything's done in action methods. But action filters could be used as well. If you can't come up with a general action filter that can serve many different state objects then it's best to just write the code in action methods.
Another approach
If you know Asp.net MVC good enough you could take this a step further and write a state machine ControllerFactory that would work along with routing in a sense as:
{StateObjectType}/{State}
ControllerFactory would therefore be able to parse view data to a known state object type and pass execution to particular action. According to state. This would make it a specially state machine suited Asp.net MVC application.
The more important question is of course whether you can create the whole application with this pattern or are there just certain parts of it that should work like this. You could of course combine both approaches and provide appropriate routing for each.
Important notices
You should be very careful how you define your error state, because entering invalid field data shouldn't result in error state but rather in data validation errors that actually display within the view beside the field with invalid data (ie. invalid date provided as 13/13/1313). Your error state should only be used for actual object state error that's not related to user input. What would that be is beyond my imagination.
As mentioned in my comment you should check out some Asp.net MVC intro videos and you'll see how validation works in Asp.net MVC. Also rather simple stuff.
State pattern of this kind is not something a regular Asp.net MVC developer would use, because it would most likely complicate code more than taking the normal approach. Analyse before you decide. Asp.net MVC is very clean code wise so adding additional abstraction over it may become confusing. And your domain model (state classes) would most likely have a much more complex code as simple POCOs with data annotations.
In your case data validation would also be more complicated (when used with data annotations) because you object should be validated according to its state which may be different between states. POCO objects are always validated the same. This may mean that we may use more classes but they are smaller, simpler and easier to maintain.
I think you are confusing states. Examples of state are:
Awaiting for a user to register
User registered successfully
User didn't register successfully
Now each of these states would have a page:
localhost:8034/Register
localhost:8034/Register/Success
localhost:8034/Register/Failure
If user can't register because they left some fields empty, they will be in the first state and you will have to display some validation messages.
Because of this, as the minimum I'll have a controller called Register and the following action methods:
Index() GET/POST
Success() GET
Failure() GET
So in order accomplish what I asked in this post I did the following:
[iPhone]
[ActionName("Index")]
public ActionResult IndexIPhone()
{
return new Test.Areas.Mobile.Controllers.HomeController().Index();
}
[ActionName("Index")]
public ActionResult Index()
{
return View();
}
Which still serves the same view as the Index action method in this controller. Even though I can see it executing the Test.Areas.Mobile.Controllers.HomeController().Index() action method just fine. What's going on here? And how do I serve the Index view from Mobile area without changing the request URL (as asked in the original post referenced above)?
You have a few options:
Redirect to the Action you'd like to return: return RedirectToAction("Action-I-Want").
Return the View by name: return View("The-View-I-Want").
Note that with the 2nd approach you'd have to put your view in the "Shared" folder for all controllers to be able to find it and return it. This can get messy if you end up putting all your views there.
As a side note: The reason your work doesn't find the view is because default view engine looks for the view in the folder that "belongs" to the current executing controller context, regardless of what code you're calling.
Edit:
It is possible to group all "mobile" views in the same folder. On your Global.asax (or where ever you're setting up your ViewEngine, just add the path to your mobile View in the AreaViewLocationFormats. Mind you, you'll still have to name your views differently.
You can also write your own view engine. I'd do something like detecting the browser and then serving the right file. You could setup a convention like View.aspx, and View.m.aspx.
Anyhow, just take a look at WebFormViewEngine and you'll figure out what works best for you.
The easiest way to send a request to a view handled by another controller is RedirectToAction("View-Name", "Controller-Name").
There are overloads of View() that take route information that might work as well, but they'd require more effort to set up.
Well actually the easiest way is to make one version of your site programmed on standards instead of browser detection :D -- however in direct response to accomplish what it in a more of a ASP.NET mvc fashion, using:
RedirectToAction("ViewName", "ControllerName");
is a good method however I have found it is more practical if you feel you must program for different browser standards to create a primary view and an alternate "mobile" view under your controllers views. Then instead of writing special code on every controller, instead extend the controller like so.
public class ControllerExtended : Controller
{
private bool IsMobile = false;
private void DetectMobileDevices(){ .... }
}
Then modify your controller classes to instead say ControllerExtended classes and just add the one line to the top of each Action that you have alternate views of like so:
public class ApplicationsController : ControllerExtended
{
// GET: /Applications/Index
public ActionResult Index() {
this.DetectMobileDevices();
if(this.IsMobile){
return RedirectToAction("MobileIndex");
} else {
// actual action code goes here
return View();
}
}
}
Alternately you can use return View("ViewName"); but from my experience you want to actually perform different actions as opposed to just showing the result in a different view as in the case of presenting an HTML table as opposed to a Flex table to help iPhone users since there is no flash support in the iPhone, etc. (as of this writing)
What is the ultimate workaround?)
Desired scenarios are:
Multistep forms.
If a page has tabs on it, the tabs should persist their `viewstate'
Whatever navigation user has chosen, it shouldn't affect (more appropriately, bother) the conversational state management.
Those are just several aspects, but I find them much practically relevant.
I have worked once on Wizard kind of form in Asp.Net MVC and best thing to do in this case is to use Model/ModelBinding to keep track of form input.
We can create a chain of Controller Actions (for each steps) with output model of each serving as the input Model for the next Step (Action).
For example if we have three steps for creating a user. Then UserController can have actions for each steps.
public ActionResult Step1()
{
User user = new User();
return View(user);
}
[Post]
public ActionResult Step1(User user)
{
//perform business validation
RedirectToAction<UserController>(u => Step2(user));
}
After this Step2 action will take over with modified user from Step1 and can render its own view and so on.
You may also want to check out http://blog.maartenballiauw.be/post/2009/10/08/Leveraging-ASPNET-MVC-2-futures-ViewState.aspx. There's an Html.Serialize() helper in MVC Futures. The article refers to it as lightweight Viewstate, but it's effectively just a glorified wrapper around "serialize this object and store the base64 string in a hidden form field." If you need state to be associated with individual pages rather than an entire Session, this helper might be appropriate for your needs.
Not sure about a "workaround", but have you considered using AJAX and jQuery? Both would be appropriate based on the requirements you listed.