MVC controller being loaded multiple times - c#

My goal:
The user session will keep track of guid's stored in
Session.Add(guid.tostring()).
When a partial is refreshed inside a div, the controller will check
for existing guids. This will let me track what notifications need
displayed, what are already being displayed to avoid duplicates ..etc.
The Problem:
When a notification should be displayed again, it isn't even though I
see it in the model being passed to the view
What I think is the cause
For some reason when I debug the controller as the very start of the
method to load the partial, it's being loaded many times which I
believe is why when a notification should be displayed, it isn't.
Main Index View that refreshes the partial. #overview. Also the interval is every 15 seconds.
function intervalTrigger() {
$('#overview').load('/Home/Overview');
};
<div id="overview">
#{Html.RenderPartial("Overview", "Home");}
</div>
Code inside Overview partial that displays the alerts
#for (int i = 0; i < Model.DisplayAlerts.Count(); i++)
{
#:$(function () {
#:$.Notify({
#:keepOpen: true,
#:caption: '#Model.DisplayAlerts[i].SectionName',
#:content: '#Model.DisplayAlerts[i].ParamDescription <br/> #Model.DisplayAlerts[i].DetailDatetime.ToString("MM/dd/yyyy hh:mm:ss tt ")',
#:type: 'alert'
#:});
#:});
}
Code that is populating the model being handed to the view
OverviewModel model = new InformationController().GetOverviewModel();
model.DisplayAlerts = new List<BasicSectionDetailModel>();
List<BasicSectionDetailModel> details = new Collector.InfoCollector().GetSectionDetailsNotOKState();
List<Guid> sessionKeys = new List<Guid>();
for (int i = 0; i < Session.Contents.Count; i++)
{
Guid guid;
if (Guid.TryParse(Session.Keys[i].ToString(), out guid))
{
sessionKeys.Add(guid);
}
}
List<BasicSectionDetailModel> addAlert = details.Where(x => !sessionKeys.Any(x2 => x2 == x.ID)).ToList();
foreach (BasicSectionDetailModel alert in addAlert)
{
Session.Add(alert.ID.ToString(), null);
}
List<Guid> removeAlert = sessionKeys.Where(x => !details.Any(x2 => x2.ID == x)).ToList();
foreach (Guid remove in removeAlert)
{
Session.Remove(remove.ToString());
}
model.DisplayAlerts = addAlert;
return model;

I found the issue
model.DisplayAlerts = addAlert;
addAlert was being destroyed. I had to copy the contents into the model and not assign one var to another.

Related

Update Model Value in a Previously Loaded Razor Partial View

I have a view that loads a set of modules (we will say other partial views) when the view renders.
So think of it like this in the master view:
<div>
#Html.PartialView("~/Views/MyApp/Partials/Module1.cshtml");
</div>
<div>
#Html.PartialView("~/Views/MyApp/Partials/Module2.cshtml");
</div>
<div>
#Html.PartialView("~/Views/MyApp/Partials/Module3.cshtml");
</div>
There is a model value that is altered in the partial view Module2.cshtml. Actually it is altered in the action for the Module2 view. I am setting the model value in public ActionResult RenderChecklist(Model pageModel, IGrouping<string, ChecklistItem> list, int checklistCount):
if (itemCounter == itemCheckedCounter)
{
priorityCounter++;
pageModel.MyAppInfo.ChecklistPriorityLevel = priorityCounter;
listRestrict = "no-restrict";
overlayShow = "hidden";
}
else
{
listRestrict = "no-restrict";
overlayShow = "hidden";
}
Depending on the ChecklistPriorityLevel value is determines if an overlay is shown in Module1, Module3, etc., but since Module1 loads before Module2, the value of of ChecklistPriorityLevel in Module1 is always initiated at 0.
The code in the partial view that is called in each module is something like this:
#if (moduleRestrict && !(moduleRestrictPriorityLevel <= checklistPriority) && !Model.GetValue<bool>("moduleRestrictBypass"))
{
const string moduleLockMessage = "This section is locked.";
<div class="module overlay show">
<img src="/assets/myapp/images/lock.png" alt="Module Lock">
<p>#moduleLockMessage</p>
</div>
}
The relative code in the model is just a regular get, set at this moment:
namespace MyApp.Core.MySite.Models
{
/// <summary>
/// Model for MySite.
/// </summary>
public class MyAppInfoModel
{
... //other models
[Ignore]
public int ChecklistPriorityLevel { get; set; }
}
}
So my question is how do I get the change in the value of this model to trigger the change in other modules (partial views) that have already loaded?
DISCLAIMER: I changed some of my actual code for privacy purposes. I am just trying to give enough information to have viewers understand what I am trying to do. I am looking for the best option, whether it is asynchronous, or whatever, to properly get the value in other partial views regardless of which partials load first.
Fix for my own issue (But I would still really like to see how this could be handled)
So for my particular problem I decided that I would just force the value of the model to be set before loading any of the modules. The way my app works, Module2 could be in any spot actually, and any modules could be ahead or behind Module2. What order the modules are in is determined in the backoffice. So I just decided to create a SurfaceController and get the data (checklists) on the main view, but then I have to get the data again in "Module2", which is my ChecklistModule. I don't really like having to iterate the checklists twice, but in order to get that ChecklistPriorityLevel value I have to iterate through the checklists.
So in my main view a call the following:
MyAppChecklistController.SetChecklistPriorityLevel(Model);
Then my method is:
public static void SetChecklistPriorityLevel(MyAppRenderModel pageModel)
{
var checklist = GetCheckList(pageModel);
var priorityCounter = 1;
foreach (var list in checklist)
{
var listCount = list.Count();
var itemData = new ValueTuple<int, int, string>(1, 1, null);
itemData = list.OrderBy(x => x.IsChecked).ThenBy(x => x.SortOrder).Aggregate(itemData,
(current, item) => GetItemList(item, current.Item1, current.Item2, current.Item3, listCount));
var itemCounter = itemData.Item1;
var itemCheckedCounter = itemData.Item2;
// var itemList = itemData.Item3;
priorityCounter = GetPriorityLevel(itemCounter, itemCheckedCounter, priorityCounter);
pageModel.ChecklistPriorityLevel = priorityCounter;
}
}
Then when I render the checklist in the ChecklistModule partial view:
[ChildActionOnly]
public ActionResult RenderChecklist(IGrouping<string, ChecklistItem> list,
int checklistCount, int checklistCounter, int priorityCounter)
{
var parentFolder = list.First().UmbracoContent.Parent;
var listCount = list.Count();
var itemData = new ValueTuple<int, int, string>(1, 1, null);
var color = GetChecklistColorValue(parentFolder);
itemData = list.OrderBy(x => x.IsChecked).ThenBy(x => x.SortOrder).Aggregate(itemData,
(current, item) => GetItemList(item, current.Item1, current.Item2, current.Item3, listCount));
var itemCounter = itemData.Item1;
var itemCheckedCounter = itemData.Item2;
var itemList = itemData.Item3;
var checklistDict = GetChecklistRestrictions(parentFolder, itemCounter,
itemCheckedCounter, priorityCounter);
checklistDict.Add("color", color);
checklistDict.Add("itemList", itemList);
checklistDict.Add("checklistCounter", checklistCounter);
return GetChecklistLayout(checklistCount, checklistLayoutDict);
}
So as you can see I am pretty much running through the checklist twice in one page load. I didn't want to have to do that. If anyone has a better idea to make this more efficient, let me know.
UPDATE: The above solution (even though only part of the code is posted) fixed my issue, however I decided that I would just catch the checklist on the view and add it to the models too, then use that model (pageModel.Checklists) in the partial, so I am not actually grabbing the checklist twice with Linq. I still have to iterate through the checklists twice, but I am not grabbing the values twice. So still more of a hack, but maybe I will keep finding a way later to streamline this.

MVC5 Accessing User in BaseController

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.

ViewModel Property Null in Post Action

This is now fixed. A combination of Ish's suggestion below plus adding calls to #HiddenFor in the view resolved the problem.
I have an ASP.NET MVC 5 web application where users can mark a defect as resolved. I want to display a list of potentially related defects, with check-boxes that users can tick to indicate that yes, this is the same defect, and should also be marked as resolved.
So I have a View Model with a property that is a collection, each member of which contains a defect object property and Boolean IsSameDefect property. This all works fine in the GET action method and in the view. I can display the related defects and tick the boxes.
The problem arises in the POST action when I want to update the data. At this point the property (the collection of potentially related defects) is null. I'm having a hard time trying to figure out how to pass this data back to the controller?
Code as requested ...
// GET: /DefectResolution/Create
public ActionResult Create(int ciid)
{
int companyId = User.CompanyID();
DefectResolutionCreateViewModel drcvm = new DefectResolutionCreateViewModel(ciid, companyId);
return View(drcvm);
}
// POST: /DefectResolution/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(DefectResolutionCreateViewModel drcvm)
{
DefectResolutions currentResolution = drcvm.DefectResolution;
currentResolution.CreatedOn = System.DateTime.Now;
currentResolution.UserID = User.UserID();
if (ModelState.IsValid)
{
unitOfWork.DefectResolutionRepository.Insert(currentResolution);
if (currentResolution.ResolutionStatusID == 2)
{
//code breaks here as drcvm.RelatedUnresolvedDefects is null
foreach (var relatedDefect in drcvm.RelatedUnresolvedDefects)
{
if (relatedDefect.IsSameDefect)
{
DefectResolutions relatedResolution = new DefectResolutions();
relatedResolution.ChecklistID = relatedDefect.RelatedChecklist.ChecklistID;
relatedResolution.CreatedOn = System.DateTime.Now;
relatedResolution.ResolutionNote = currentResolution.ResolutionNote;
relatedResolution.ResolutionStatusID = currentResolution.ResolutionStatusID;
relatedResolution.UserID = User.UserID();
}
}
}
unitOfWork.Save();
return RedirectToAction("Index", new { ciid = currentResolution.ChecklistID });
}
return View(drcvm);
}
In the view ...
#model Blah.ViewModels.DefectResolution.DefectResolutionCreateViewModel
#{
ViewBag.Title = "Create Defect Resolution";
var relatedDefects = Model.RelatedUnresolvedDefects;
}
... and later in the view ...
#for (int i = 0; i < relatedDefects.Count(); i++ )
{
<tr>
<td>
#Html.EditorFor(x => relatedDefects[i].IsSameDefect)
</td>
</tr>
}
I followed Ish's suggestion below, and modified the code to refer to Model.RelatedUnresolvedDefects directly instead of using a variable as I had been doing. This does get me a bit further. The view model's RelatedUnresolvedDefects property is no longer null. But only RelatedUnresolvedDefects.IsSameDefect has a value. RelatedUnresolvedDefects.RelatedChecklist is null. Here's the controller code again showing where it now breaks ...
// POST: /DefectResolution/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(DefectResolutionCreateViewModel drcvm)
{
DefectResolutions currentResolution = drcvm.DefectResolution;
currentResolution.CreatedOn = System.DateTime.Now;
currentResolution.UserID = User.UserID();
if (ModelState.IsValid)
{
unitOfWork.DefectResolutionRepository.Insert(currentResolution);
if (currentResolution.ResolutionStatusID == 2)
{
//prior to change, code used to break here
foreach (var relatedDefect in drcvm.RelatedUnresolvedDefects)
{
if (relatedDefect.IsSameDefect)
{
DefectResolutions relatedResolution = new DefectResolutions();
//code now breaks here because relatedDefect.RelatedChecklist is null
relatedResolution.ChecklistID = relatedDefect.RelatedChecklist.ChecklistID;
relatedResolution.CreatedOn = System.DateTime.Now;
relatedResolution.ResolutionNote = currentResolution.ResolutionNote;
relatedResolution.ResolutionStatusID = currentResolution.ResolutionStatusID;
relatedResolution.UserID = User.UserID();
}
}
}
unitOfWork.Save();
return RedirectToAction("Index", new { ciid = currentResolution.ChecklistID });
}
return View(drcvm);
}
Without knowing your code.I suggest you to use for loop instead of foreach while rendering the defects in View (.cshtml).
Editing Answer based on your code.
Following statement in the view creating problem
var relatedDefects = Model.RelatedUnresolvedDefects;
You should directly iterate over the Model.RelatedUnresolvedDefects property in the loop.
#for (int i = 0; i < Model.RelatedUnresolvedDefects.Count(); i++ )
{
<tr>
<td>
#Html.EditorFor(x => Model.RelatedUnresolvedDefects[i].IsSameDefect)
</td>
</tr>
}

One property with a list of model errors in mvc c#

In my controller which is having a list (atmost 3) error messages related to password check will be stored in the property named Password.
IEnumerable<PasswordMessages> passwordMessage = LoanTrackerServices.CheckPasswordRequirements(model.NewPassword, model.EmailId);
if ( passwordMessage.Count() > 0 )
{
foreach (PasswordMessages pm in passwordMessage)
{
ModelState.AddModelError("Password",( pm.Message));
}
LoginPageModel loginModel = new LoginPageModel();
return View("Index", new HomePageModel() { Register = model, Login = loginModel });
}
But in my view i am unable to figure out how to get all those (atmost 3) error messages. Right now only the first message with in the list is displaying. here is my code in view
for (int i = 0; i < ViewData.ModelState["Password"].Errors.Count; i++)
{
#Html.ValidationMessage("Password")
}
How to get all those error messages which are stored with in the Password?
You just need a summary instead of a message:
#Html.ValidationSummary("Password")
So get rid of this:
for (int i = 0; i < ViewData.ModelState["Password"].Errors.Count; i++)
{
#Html.ValidationMessage("Password")
}
#Html.ValidationSummary()
Will show all Model errors in one place what isn't desirable for the most cases.
I've written a helper method which will let you show only errors for specific field: https://gist.github.com/DimaSalakhov/8548393. It's based on standart ValidationMessageFor<>().
Add it to your project, referrence on a View and use as follows:
#Html.ValidationSummaryFor(x => x.Password)
#if(ViewData.ModelState.IsValidField(nameof(Model.Property)))
{
// show some thing
}
else
{
// show some thing else
}
Strongly typed validation check .

DropDownListFor SelectedValue and Disable using Session State

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

Categories