I'm working on an ASP.Net MVC 3 site. The _Layout master view contains a menu and I want to hide some of the items in the menu based on if you are logged in and what roles you are in.
This currently works using code like this
#if (HttpContext.Current.User.Identity.IsAuthenticated)
{
<li id="MyLearningTab">#Html.ActionLink("My Learning", "MyLearning", "Learning")</li>
if (HttpContext.Current.User.IsInRole("Reporters"))
{
<li id="ReportTab">#Html.ActionLink("Reports", "Index", "Reports")</li>
}
if (HttpContext.Current.User.IsInRole("Administrators"))
{
<li id="DashboardTab">#Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>
<li id="AdminTab">#Html.ActionLink("Admin", "Index", "Admin")</li>
}
}
I'd like to refactor this in to something more readable and came up with something like this
#if ((bool)ViewData["MenuMyLearning"]){<li id="MyLearningTab">#Html.ActionLink("My Learning", "MyLearning", "Learning")</li> }
#if((bool)ViewData["MenuReports"]){<li id="ReportTab">#Html.ActionLink("Reports", "Index", "Reports")</li>}
#if ((bool)ViewData["MenuDashboard"]){<li id="DashboardTab">#Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>}
#if ((bool)ViewData["MenuAdmin"]){<li id="AdminTab">#Html.ActionLink("Admin", "Index", "Admin")</li>}
I originally added the following to my base controller constructor thinking I could setup the ViewData for these properties there
ViewData["MenuDashboard"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuAdmin"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuReports"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Reportors");
ViewData["MenuMyLearning"] = User != null && User.Identity.IsAuthenticated;
However it turns out the User object is null at this point in the lifecycle. I've also tried creating a custom global filter but the ViewData is then not accessable.
What is the recommended way of doing something like this? Should I just leave it how it was at first with all the HttpContext code in the view?
General advice about roles
The way I have done this is to create a custom principal and to store the extra required information in there. In your example, this would at least include the roles for the user. That way you avoid making lots of extra trips to the user store (which is likely a SQL database).
Have a look a this question of mine in which I give the code which I am using successfully: Is this Custom Principal in Base Controller ASP.NET MVC 3 terribly inefficient?
Note that I am storing the custom principal in the cache rather than in the session (just being paranoid about session hijacking).
I like this approach as it is very extensible. For example, I have since extended this to expose Facebook credentials for when the user logs in via Facebook.
Just remember that if you are caching data you need to remember to update it when it changes!
Answer to your question
Just to add, in your specific case, you should probably store this extra information in a ViewModel and then your view would say things like:
#if(ShowReports) { <li id="ReportTab">#Html.ActionLink("Reports", "Index", "Reports")</li> }
#if(ShowDashboard) { <li id="DashboardTab">#Html.ActionLink("Dashboard", "Dashboard", "Admin")</li> }
#if(ShowAdmin { <li id="AdminTab">#Html.ActionLink("Admin", "Index", "Admin")</li> }
with the ViewModel Code saying something like:
public bool ShowReports {get;set;}
public bool ShowDashboard {get;set;}
public bool ShowAdmin {get;set;}
public void SetViewModel()
{
if (User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.IsInRole("Reporters"))
{
ShowReports = true;
}
if (HttpContext.Current.User.IsInRole("Administrators"))
{
ShowDashboard = true;
ShowAdmin = true;
}
}
}
I actually tend to take this one step further and create a ReportsLink in my ViewModel and set it to contain the link if the user is authorised or to be an empty string if they are not. Then the view just says:
#Model.ReportsLink
#Model.DashboardLink
#Model.AdminLink
In that case the pertinent part of the ViewModel might be like this:
ReportLink = new MvcHtmlString(HtmlHelper.GenerateLink(HttpContext.Current.Request.RequestContext, System.Web.Routing.RouteTable.Routes, "linktext", "routename", "actionname", "controllername", null, null));
Here is what I ended up doing. I created a helper class called MenuSecurity with static boolean properties for each menu item showing which items should be visible. Each property looked like this
public static bool DashboardVisible
{
get
{
return
HttpContext.Current.User != null &&
HttpContext.Current.User.Identity.IsAuthenticated;
}
}
I then tidied up my menu partial view to look like this
#if (MenuSecurity.ReportsVisible){<li id="ReportTab">#Html.ActionLink("Reports", "Index", "Reports")</li>}
#if (MenuSecurity.DashboardVisible){<li id="DashboardTab">#Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>}
#if (MenuSecurity.AdminVisible){<li id="AdminTab">#Html.ActionLink("Admin", "Index", "Admin")</li>}
Create a partial view and return the view from the controller:
LayoutViewModel.cs:
public class LayoutViewModel
{ ...
public bool ShowAdmin { get; set; }
}
LayoutController.cs:
public PartialViewResult GetAdminMenu()
{
LayoutViewModel model = new LayoutViewModel();
model.ShowAdmin = userHasPermission("Admin"); // change the code here
return PartialView("_AdminMenu", model);
}
_AdminMenu.cshtml (partial view):
#model DelegatePortal.ViewModels.LayoutViewModel
#if (#Model.ShowAdmin)
{
<!-- admin links here-->
}
_Layout.csthml (main view):
...
#Html.Action("GetAdminMenu", "Layout")
Related
I'm using the following pattern https://github.com/filamentgroup/Ajax-Include-Pattern
to load partial views through ajax.
View:
#using(Html.BeginUmbracoForm("PostContactInformation", "JoiningSurface", null, new Dictionary<string, object> { { "class", "joinform" } })) {
#Html.AntiForgeryToken()
<div data-append="#Url.Action("RenderJoiningContactInformation", "JoiningSurface", new { ContentId = CurrentPage.Id })"></div>
}
With Action:
public ActionResult RenderContactInformation(int ContentId)
{
var viewModel = ContactViewModel();
viewModel.Content = Umbraco.TypedContent(ContentId);
return PartialView("RenderContactInformation", viewModel);
}
Loads partial view perfectly.
// No need to add partial view i think
Post action works correctly as well:
public ActionResult PostContactInformation(ContactViewModel model)
{
//code here
return RedirectToUmbracoPage(pageid);
}
The problem is, that i need to add model error to CurrentUmbracoPage if it exists in post...
For example:
public ActionResult PostContactInformation(ContactViewModel model)
{
ModelState.AddModelError(string.Empty, "Error occurred");
return CurrentUmbracoPage();
}
In this case i get null values for current model. And this happens only when i use ajax.
If i load action synchronously like that:
#using(Html.BeginUmbracoForm("PostJoiningContactInformation", "JoiningSurface", null, new Dictionary<string, object> { { "class", "joinform" } })) {
#Html.AntiForgeryToken()
#Html.Action("RenderContactInformation", "JoiningSurface", new { ContentId = CurrentPage.Id })
}
everything works like it should.
But i need to use ajax. Is there a correct way to pass values on postback in this case? I know that i can use TempData, but i'm not sure that this is the best approach.
Thanks for your patience
The problem is that Umbraco context is not accessible when you're trying to reach it through ajax call. Those calls are a little bit different.
Check my answer in this thread: Umbraco route definition-ajax form and I suggest to go with WebAPI and UmbracoApiControllers to be able to access those values during the Ajax call.
I am building a solution with Sitecore 7 and ASP.NET-MVC 3 and trying to use a custom model class as described in this blog post by john west.
I have seen several other questions here on SO reporting a similar error with ASP.NET-MVC (without Sitecore), usually related to passing the wrong type of object in controller code, or there being a configuration error with the \Views\web.config file, but neither seem to be the issue here.
this issue is caused when you create a view rendering (possibly others but i haven't tried it) and you have not set up the model in sitecore, so sitecore is passing in its default model.
To fix this you have to go to the layouts section and create a model.
this is the path in sitecore '/sitecore/layout/Models/', in this folder create a 'Model' item and in the model type field you add the reference to your model in the format 'my.model.namespace, my.assembly' without the quotes.
your model needs to inherit 'Sitecore.Mvc.Presentation.IRenderingModel' which forces you to implement the 'Initialize' method, in here you populate data from the sitecore item into the properties of the model. here is an example model...
namespace Custom.Models.ContentBlocks
{
using Sitecore.Data.Fields;
using Sitecore.Mvc.Presentation;
public class BgImageTitleText : IRenderingModel
{
public string Title { get; set; }
public string BgImage { get; set; }
public string BgImageAlt { get; set; }
public string BgColour { get; set; }
public string CtaText { get; set; }
public string CtaLink { get; set; }
public void Initialize(Rendering rendering)
{
var dataSourceItem = rendering.Item;
if (dataSourceItem == null)
{
return;
}
ImageField bgImage = dataSourceItem.Fields[Fields.ContentBlocks.BgImageTitleTextItem.BgImage];
if (bgImage != null && bgImage.MediaItem != null)
{
this.BgImageAlt = bgImage.Alt;
this.BgImage = Sitecore.Resources.Media.MediaManager.GetMediaUrl(bgImage.MediaItem);
}
var title = dataSourceItem.Fields[Fields.ContentBlocks.BgImageTitleTextItem.Title];
if (title != null)
{
this.Title = title.Value;
}
var link = (LinkField)dataSourceItem.Fields[Fields.ContentBlocks.BgImageTitleTextItem.CtaLink];
if (link != null)
{
this.CtaLink = link.GetLinkFieldUrl();
}
var ctaText = dataSourceItem.Fields[Fields.ContentBlocks.BgImageTitleTextItem.CtaText];
if (ctaText != null)
{
this.CtaText = ctaText.Value;
}
var bgColour = dataSourceItem.Fields[Fields.ContentBlocks.BgImageTitleTextItem.BgColour];
if (bgColour != null)
{
this.BgColour = bgColour.Value;
}
}
}
}
Then you have to go to your view rendering (or possibly other types of rendering) and in the 'Model' field you click insert link and click on your newly created model.
This error can be caused when a controller rendering invokes a controller method which returns an ActionResult object instead of a PartialViewResult. In my case I had a rendering model associated with the layout which I believe Sitecore was trying to pass to my controller rendering.
RenderingModel is used when you create a Rendering based on the View Rendering template. This model is created by the sitecore MVC pipelines and is automatically assigned to the view.
To have control over what model to bind to the view, you probably want to use a Controller Rendering, then you can pass in your own model from your controller.
Is there a way to tell what view a controller action is being called from?
For example, I would like to use "ControllerContext.HttpContext.Request.PhysicalPath" but it returns the path in which the controller action itself is located:
public ActionResult HandleCreateCustomer()
{
// Set up the customer
//..code here to setup the customer
//Check to see of the calling view is the BillingShipping view
if(ControllerContext.HttpContext.Request.PhysicalPath.Equals("~/Order/BillingShipping"))
{
//
return RedirectToAction("OrderReview", "Order", new { id = customerId });
}
else
{
return RedirectToAction("Index", "Home", new { id = customerId });
}
}
If you have a fixed number of locations that it could possibly be called from, you could create an enum where each of the values would correspond to a place where it could have been called from. You'd then just need to pass this enum value into HandleCreateCustomer, and do your condition statement(s) based on that.
At the moment I am using something of the sort:
In the View I am populating a TempData variable using:
#{TempData["ViewPath"] = #Html.ViewVirtualPath()}
The HtmlHelper method ViewVirtualPath() is found in the System.Web.Mvc.Html namespace (as usual) and is as follows and returns a string representing the View's virtual path:
public static string ViewVirtualPath(this HtmlHelper htmlHelper)
{
try{
return ((System.Web.WebPages.WebPageBase)(htmlHelper.ViewDataContainer)).VirtualPath;
}catch(Exception){
return "";
}
}
I will then obviously read the TempData variable in the controller.
I found another way.
In the controller you want to know what page it was called from.
I added the following in my controller
ViewBag.ReturnUrl = Request.UrlReferrer.AbsolutePath;
Then in the View I have a 'Back' button
#(Html.Kendo().Button().Name("ReturnButton")
.Content("Back to List").Events(e => e.Click("onReturn"))
.HtmlAttributes(new { type = "k-button" })
)
Then the javascript for the onReturn handler
function onReturn(e) {
var url = '#(ViewBag.ReturnUrl)';
window.location.href = url;
}
I'm sorry, for I'm sure there's a way to do this with a viewModel, however I'm very inexperienced with this and don't even know if I'm doing it correctly.
What I'm trying to do is pass multiple blogs and the profile info of the user who posted each blog to a view.
I'm getting the following error.
The model item passed into the dictionary is of type
'ACapture.Models.ViewModels.BlogViewModel', but this dictionary
requires a model item of type
'System.Collections.Generic.IEnumerable`1[ACapture.Models.ViewModels.BlogViewModel]'.
I'm trying to pass the following query results to the view.
var results = (from r in db.Blog.AsEnumerable()
join a in db.Profile on r.AccountID equals a.AccountID
select new { r, a });
return View(new BlogViewModel(results.ToList()));
}
This is my viewModel
public class BlogViewModel
{
private object p;
public BlogViewModel(object p)
{
this.p = p;
}
}
And my view
#model IEnumerable<ACapture.Models.ViewModels.BlogViewModel>
#{
ViewBag.Title = "Home Page";
}
<div class="Forum">
<p>The Forum</p>
#foreach (var item in Model)
{
<div class="ForumChild">
<img src="#item.image.img_path" alt="Not Found" />
<br />
<table>
#foreach (var comment in item.comment)
{
<tr><td></td><td>#comment.Commentation</td></tr>
}
</table>
</div>
}
</div>
Thanks in advance.
I guess you need to change your view model a little to:
public class BlogViewModel
{
public Blog Blog { get; set; }
public Profile Profile{ get; set; }
}
and then return it as follow:
var results = (from r in db.Blog.AsEnumerable()
join a in db.Profile on r.AccountID equals a.AccountID
select new new BlogViewModel { Blog = r, Profile = a });
return View(results.ToList());
Then in your foreach loop inside of view, you will get an objects that will contain both - profile and blog info, so you can use it like f.e. #item.Profile.Username
I'm not entirely sure what you're trying to accomplish with the ViewModel in this case, but it seems like you are expecting for the page to represent a single blog with a collection of comments. In this case you should replace
IEnumerable<ACapture.Models.ViewModels.BlogViewModel>
With
ACapture.Models.ViewModels.BlogViewModel
Then Model represents a single BlogViewModel, that you can iterate over the comments by using Model.comments and access the image using Model.image.img_path.
If this not the case, and you intend to have multiple BlogViewModels per page, then you will have to actually construct a collection of BlogViewModels and pass that to the view instead.
Consider, for example's sake, the logic "A user may only edit or delete a comment that the user has authored".
My Controller Actions will repeat the logic of checking whether the currently logged in user can affect the comment. Example
[Authorize]
public ActionResult DeleteComment(int comment_id)
{
var comment = CommentsRepository.getCommentById(comment_id);
if(comment == null)
// Cannot find comment, return bad input
return new HttpStatusCodeResult(400);
if(comment.author != User.Identity.Name)
// User not allowed to delete this comment, return Forbidden
return new HttpStatusCodeResult(403);
// Error checking passed, continue with delete action
return new HttpStatusCodeResult(200);
}
Of course, I can bundle that logic up in a method so that I'm not copy / pasting that snippet; however, taking that code out of the controller and putting it in a ValidationAttribute keeps my Action smaller and easier to write tests for. Example
public class MustBeCommentAuthorAttribute : ValidationAttribute
{
// Import attribute for Dependency Injection
[Import]
ICommentRepository CommentRepository { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
int comment_id = (int)value;
var comment = CommentsRepository.getCommentById(comment_id);
if(comment == null)
return new ValidationResult("No comment with that ID");
if(comment.author != HttpContext.Current.User.Identity.Name)
return new ValidationResult("Cannot edit this comment");
// No errors
return ValidationResult.Success;
}
}
public class DeleteCommentModel
{
[MustBeCommentAuthor]
public int comment_id { get; set; }
}
Is Model Validation the right tool for this job? I like taking that concern out of the controller Action; but in this case, it may complicate things further. This is especially true when you consider that this Action is part of a RESTful API and needs to return a different HTTP Status Code depending on the Validation errors in the ModelState.
Is there "best practice" in this case?
Personally, I think that it looks nice, but you are getting carried away with annotations. I think that this does not belong in your presentation layer and it should be handled by your service layer.
I would have something on the lines of:
[Authorize]
public ActionResult DeleteComment(int comment_id)
{
try
{
var result = CommentsService.GetComment(comment_id, Auth.Username);
// Show success to the user
}
catch(Exception e)
{
// Handle by displaying relevant message to the user
}
}