I am building a mail system where at every page that you land you will get a notification that you have unread mail.
As this needs to be on every page, I thought that I should probably then just add functionality to Base Controller and have the function called that way as every controller I have will be extended of my Base Controller.
As such in my base controller I have the following function which will get me the number of unread invitations this user has:
public void GetUnreadInvitationCount(string userId)
{
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).Count();
if (count > 0) ViewBag.UnreadInvitations = count;
}
Then in my constructor I tried the following:
public class BaseController : Controller
public BaseController()
{
if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
{
GetUnreadInvitationCount(User.Identity.GetUserId());
}
}
}
The problem is that the User is null as it has not been instantiated.
How do I get around this? How do I make a common functionality such as this be on every page and not have to repeat my code on every controller specifically?
I have thought of few solutions myself, but none of these seem to be the right way to go.
Option 1: Create a BaseViewModel which will be called in every page that has this value, this would mean I have to instantiate the method in every action on the website, but at least the code is common for it if I ever need to update it.
Option 2: Do not do this on the server side but setup an ajax script to be called after the page has loaded. This would have an initial delay but it would work.
Does anyone has a different solution?
EDIT - For JohnH:
I have tried solution suggested by john, here is the code:
_Layout.cshtml
#{ Html.RenderAction("GetUnreadInvitationCount", "Base");}
BaseController.cs
public ActionResult GetUnreadInvitationCount()
{
string userId = User.Identity.GetUserId();
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).OrderByDescending(r => r.Id).Count();
BaseViewModel model = new BaseViewModel {RequestCount = count};
return View("UnreadInvitations", model);
}
UnreadInvitations.cshtml
#model Azularis.System.Events.Models.ViewModels.BaseViewModel
#if (#Model.RequestCount > 0)
{
<li>
#Html.ActionLink("Mail", "Index", "Teams", null, new { #class = "mail-image" })
#Html.ActionLink(#Model.RequestCount.ToString(), "Index", "Teams", null, new { #class = "mail-number" })
</li>
}
However this forces me into a loop where _Layout.cshtml is constantly repeating until the page crashes with
The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.
Does anyone knows why it constantly loops?
As discussed in the comments above, the real issue here is not that the code should be shared amongst various controllers, it's that you want a common point in which to run your particular piece of code. In that sense, it lends itself to being abstracted out into a separate controller, which centralises all invitation logic in one place, leading to better separation of concerns. You can then invoke those actions either in your _Layout.cshtml view, or in any other views if need be.
Using the code in your answer as an example (thanks for that):
InvitationController:
public ActionResult GetUnreadInvitationCount()
{
string userId = User.Identity.GetUserId();
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).OrderByDescending(r => r.Id).Count();
BaseViewModel model = new BaseViewModel {RequestCount = count};
return View("UnreadInvitations", model);
}
InvitationController\UnreadInvitations.cshtml:
#if (Model.RequestCount > 0)
{
// Render whatever you need to display the notification
}
Then finally, in your _Layout.cshtml, somewhere, you would invoke this action by calling:
#{ Html.RenderAction("GetUnreadInvitationCount", "Invitation"); }
It's important to note that you may need to use #{ Layout = null; } in the child view being rendered, otherwise it will default to rendering _Layout.cshtml again... which in turn renders the action again... then calls the child view again... and so on. :) Setting the layout to null will prevent that from happening.
Edit: Actually, the reason the _Layout.cshtml file is being called again is because we're returning a ViewResult from the action. Change that to a PartialViewResult and you no longer need the #{ Layout = null; }. Thus:
return View("UnreadInvitations", model);
becomes:
return PartialView("UnreadInvitations", model);
User property is null because it is set after constructor is invoked. However, you do not have to do your logic in the constructor. The following should be placed in your BaseController.
protected int? GetUserId()
{
return (User != null && User.Identity != null && User.Identity.IsAuthenticated) ? User.Identity.GetUserId() : null;
}
protected void GetUnreadInvitationCount()
{
int? userId = GetUserId();
if (userId == null)
throw new SecurityException("Not authenticated");
var count = Db.Request.Where(r => r.ReceiverId == userId.value && r.DateLastRead == null).Count();
if (count > 0) ViewBag.UnreadInvitations = count;
}
GetUnreadInvitationCount is called after User is initialized (I guess when some controller action is gets called) and can use GetUserId from the BaseController.
Related
I have a method in my controller class that returns a tuple and runs right before the rest of the controller executes:
private async Task<(int userId, int userRoleId, bool accountVerified)> GetUserRole()
{
var identifier = User.Identity.Name;
var user = await _context.User.Where(x => x.UserEmail == User.identifier).FirstOrDefaultAsync();
//this is an oddly specific condition that differs for every controller class. Causes a compiler error as written
if (!user.Approved && (user.RoleId == 1 || user.RoleId == 7))
{
return RedirectToAction("Index", "Notifications");
}
//this returns data that is used in the controller methods
else
{
return (user.UserId, user.RoleId, user.Approved);
}
}
I have several controller classes where this method is declared. Currently I have the above mentioned redirect declared in each view method (Index(), Create(), Edit(), etc). It seems a bit of a waste to declare a new authorization policy for every controller that needs this when the logic is already there.
Is there a way to redirect the user without declaring return RedirectToAction(...)?
Or am I missing something entirely?
fairly newbie question here. I am creating a poll system in asp.net
In my view I have the following option:
#using (Html.BeginForm("Vote","Poll"))
{
#Html.LabelFor(m => m.option_Pal);
#Html.RadioButtonFor(m=>m.option_Pal, true)
<button type="submit" class="btn btn-primary">Vote!</button>
}
Then I receive said data in this method in my PollController:
[HttpPost]
public ActionResult Vote(Poll poll)
{
if (Request.Form["m.option_Pal"] != null)
{
palResult.dailyVotesCounter++; //global counter where I store every vote
}
return RedirectToAction("Result", "Poll");
}
palResult is initialized in the beginning of my PollController:
public class PollController : Controller
{DailyResult palResult = new DailyResult();
[...]
And finally:
public ActionResult Result()
{
ViewBag.Message = "Pals : " + palResult.dailyVotesCounter;
return View();
}
I have tried several ways but I can't get the palResult.dailyVotesCounter to increase when it's option is marked in the view. What am I doing wrong?
Thanks in advance!
The comment on the variable says it's a global counter, but MVC is stateless and as such you have to persist the value. When the code does RedirectToAction() after incrementing the value, that value is lost (palResult object is destroyed) unless persisted in some mechanism like Session, cookie, passing it on the redirect as a querystring variable, etc.
So the easy way could have been:
return RedirectToAction("Result", "Poll", new { count = palResult.dailyVotesCounter });
However, that exposes the count to the user.
I am allowing users to upload a file from the view which will eventually be passed to the model through controller actions. However, there is a particular situation that is giving me trouble.
I have the followong ViewModel that I'm passing to the view:
public class RegisterStudentViewModel
{
public Login loginViewModel;
public Person personViewModel;
public PersonResume personResViewModel;
}
When the form is submitted, I use some validation logic to make sure the fields entered were applicable, and if any of the validation fails, I send the ViewModel that I'm using back to the view, to make sure that the fields that had information entered correctly are stored. Before this though, I have also made sure that the file uploaded is binded to the model that it belongs to, like this:
if (res != null)
{
PersonResume resumeViewModel = createResume(res, personModel);
vm.personResViewModel = resumeViewModel;
}
SetUpView(vm, da);
result = View(vm);
}
so that when the validation fails but the resume wasn't one of the fields that failed and I want to display the uploaded resume, I can use the following code in my view:
#if (Model.personResViewModel != null && Model.personViewModel.id == 0)
{
<th class="editAddLabel">
#Html.LabelFor(model => model.personResViewModel.ResumeFileName, "Resume File Name")
</th>
<td>
#Html.ActionLink("View your resume", "linkDownload", "Student", new { id = Model.personViewModel.id, resume = Model.personResViewModel}, null)
</td>
}
to pass to the following controller method that will allow the link to be downloaded:
public FileResult linkDownload(int id, PersonResumeViewModel resume)
{
DataAccess da = new DataAccess();
PersonResume displayResume = new PersonResume();
if (id != 0)
{
displayResume = da.getPersonResumeByID(id);
}
else
{
displayResume.ResumeData = resume;
}
//some more code that downloads file
}
I know that in the view, the personResViewModel is not null, but when I look at the controller method, it is null and I can't do anything with it. Am I not passing my argument in correctly?
I am writing an MVC 5 app with a relatively complicated data model.
I have Listings and Listings have photo Albums associated with them.
To get things started, I just made sure that when a user is trying to call the Edit function of a controller, that the user was the owner of the object. Like so:
// Listing Controller
public bool VerifyOwnership(int? id)
{
if (id == null) return false;
Listing listingModel = db.Listings.Find(id);
if (listingModel == null)
{
return false;
}
else
{
return User.Identity.GetUserId() == listingModel.SellerID;
}
}
However, this check is now propagating itself throughout my code base. Since Albums are owned by Listings, this code didn't seem that terrible to me:
// AlbumController
public ActionResult Edit(int? id, int listingId)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Album a = db.Albums.Find(id);
if (a == null)
{
return HttpNotFound();
}
var l = new ListingController();
if (!l.VerifyOwnership(listingId))
return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
ViewBag.ListingID = listingId;
return View(a);
}
I think I'm doing it wrong.
It seems that ideally the Album controller would not be instantiating a ListingController just to check ownership. I could copy the ownership logic out of the ListingController and paste it into the AlbumController, but now I'm copy pasting code. Yuck.
I read this article about making a custom Authorize attribute - ASP.NET MVC Attribute to only let user edit his/her own content, which seems ok except that I wasn't sure how to instantiate an ApplicationDbContext object inside the AuthorizeCore override so that I could lookup the owner of the listing and do my checks. Is it ok to just create ApplicationDbContext objects willy-nilly? Do they correlate to persistent database connections or are they an abstraction?
This is where you're going wrong....
Album a = db.Albums.Find(id);
I could have typed in any ID and your app would go and fetch it and then perform a ownership verification which is unneeded...
Tackle the problem from the other end, what if, we were to scope our results by what the users has access to first, and then performing a search for an album within the scope of what the user has access to. Here's a few examples to give you an idea of what I mean.
db.Users.Where(user => user.Id == this.CallerId)
.SelectMany(user => user.Albums)
.Where(album => album.Id == "foo");
db.Albums.Where(album => album.OwnerId == this.CallerId)
.Where(album => album.Id == "bar");
It's all going to come down to the layout of your db and the fashion in which you've mapped your entity models, but the concept is the same.
I have been introduced to Razor as applied with MVC 3 this morning, so please forgive me if my question seems terribly uninformed!
I am working with an app whose workflow involves allowing a user to select a value (warehouse) from a drop down list, and add a record (material) from that warehouse to another record (Materials Request). Once the first material has been added to the Materials Request, I need to permanently set the value of the drop down to the warehouse that was first selected, then disable the drop down control (or set to read only, perhaps). The existing code in the razor file uses the DropDownListFor() method, including a ViewBag collection of Warehouse records. I have seen discussions which suggest abandoning the ViewBag design, but honestly I don't have the desire to rewrite major portions of the code; at least it looks like a major rewrite from the perspective of my experience level. Here's the original code:
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
I believe I have been able to select a value based on a session object, though I'm still not sure how to disable the control. Here's my change:
#{
int SelectedWarehouseId = -1;
if (HttpContext.Current.Session["SelectedWarehouseId"] != null)
{
SelectedWarehouseId = Int32.Parse(HttpContext.Current.Session["SelectedWarehouseId"].ToString());
}
}
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#{
if (SelectedWarehouseId > -1)
{
#Html.DropDownListFor(m => m.WarehouseId, new SelectList((IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "WarehouseId", "WarehouseDescription", (int)SelectedWarehouseId))<br />
}
else
{
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
}
}
When the material is added to the Material Request, the WarehouseId is passed to the controller and I can access that value as "model.WarehouseId" in the controller class. However, I'm not sure how to get that value back to the View (apologies for the large code block here):
[HttpPost]
[TmsAuthorize]
public ActionResult Create(ItemRequestViewModel model)
{
string deleteKey = null;
//Removed code
else if (Request.Form["AddToRequest"] != null)
{
// If the user clicked the Add to Request button, we are only
// interested in validating the following fields. Therefore,
// we remove the other fields from the ModelState.
string[] keys = ModelState.Keys.ToArray();
foreach (string key in keys)
{
if (!_addToRequestFields.Contains(key))
ModelState.Remove(key);
}
// Validate the Item Number against the database - no sense
// doing this if the ModelState is already invalid.
if (ModelState.IsValid)
{
_codes.ValidateMaterial("ItemNumber", model.ItemNumber, model.WarehouseId);
Session["SelectedWarehouseId"] = model.WarehouseId;
}
if (ModelState.IsValid)
{
// Add the new Item Request to the list
model.Items.Add(new ItemViewModel() { ItemNumber = model.ItemNumber, Quantity = model.Quantity.Value, WarehouseId = model.WarehouseId });
ModelState.Clear();
model.ItemNumber = null;
model.Quantity = null;
model.WarehouseId = null;
}
}
//Removed code
return CreateInternal(model);
}
private ActionResult CreateInternal(ItemRequestViewModel model)
{
if (model != null)
{
if (!String.IsNullOrEmpty(model.SiteId))
{
ViewBag.BuildingCodes = _codes.GetBuildingCodes(model.SiteId, false);
if (!String.IsNullOrEmpty(model.BuildingId))
ViewBag.LocationCodes = _codes.GetLocationCodes(model.SiteId, model.BuildingId, false);
}
//Removed code
}
//Removed code
ViewBag.WarehouseCodes = _codes.GetWarehouseCodes(false);
return View("Create", model);
}
So my questions are, how do I disable the drop down list, and how can I pass a value for the selected WarehouseId back to the view? I've also considered adding the value to the ViewBag, but to be honest I don't know enough about the ViewBag to recognize any unintended consequences I may face by just randomly modifying it's contents.
Thanks for any help offered on this.
Without going into which approach is better...
Your dropdown should be rendered as an HTML select element, in order to disable this you'll need to add a disabled="disabled" attribute to it.
The DropDownListFor method has a htmlAttributes parameter, which you can use to achieve this:
new { disabled = "disabled" }
when your pass model to your view like
return View("Create", model);
if WareHouseID is set in model then
Html.DropDownListFor(x=>x.WareHouseID, ...)
will automatically set the selected value and u don't have to do that session processing for this. So far as disabling a field is required, stewart is right. you can disable drop down this way but then it won't be posted to the server when u submit the form. you can set it to readonly mode like
new{#readonly = "readOnly"}