Handling Next Record nullable reference Controller Code to pass to viewbag - asp.net-mvc

I have researched various nullable reference handling posts, but not finding anything helpful. So what I am doing below to handle this null reference (it's a hack for now to stop the error page from displaying to users) is to essentially return the current id if a next record does not exist in my edit controller.
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var myInClause = new string[] { "a", "c", "k" };
var myQueryResults = await _context.MyClass.FindAsync(id);
int? NextIdObject = (from i in _context.MyClass
where myInClause.Contains(i.RowType) && (i.Id > myclass.Id)
select new { i.Id }).DefaultIfEmpty().First().Id;
if (!NextIdObject.Equals(0))
{
ViewBag.nextID = NextIdObject;
}
else
{
ViewBag.nextID = id;
}
if (myQueryResults == null)
{
return NotFound();
}
return View(myQueryResults);
}
I would prefer to just redirect to the index page (if they hit this error, it means they are done working through a queue anyway, no next record would ever exist here). Or maybe just keep the code as is and display a message on the button to indicate end of list. Any thoughts here. Note, using any +1 increment on the id does not work for me, as I don't need the user to see all id records, just the one's with a/c/k which is why I bring the myInclause variable in. If there is a better way to use the SQL sytanx of "IN" for Linq queries, I am all ears.

I would prefer to just redirect to the index page (if they hit this
error, it means they are done working through a queue anyway, no next
record would ever exist here)
You could use try...catch block simply like
try
{
var myInClause = new string[] { "a", "c", "k" };
var myQueryResults = await _context.MyClass.FindAsync(id);
int NextIdObject = (from i in _context.MyClass
where myInClause.Contains(i.RowType) && (i.Id > myclass.Id)
select new { i.Id }).DefaultIfEmpty().First().Id;
ViewBag.nextID = NextIdObject;
}
catch(Exception ex)
{
return RedirectToAction("Index");
//or return View("Index");
}
return View(myQueryResults);

Related

Error checking in controller in ASP.NET Core MVC

I'm discovering ASP.NET Core MVC and I'm on my first project. Creating a cool web shop.
I'm currently wondering how to implement faulty information checking for example in the controller
Let's say there a product page, whenever users clicks on a product they will hit the function below.
As you can see the function accepts an int parameter named id, it will search in the database for the id that fits the productId, but I'm wondering how do I add error checking here? Like for example if the id does not exist in database return to page XX?
Also feel free to give suggestions to the function if you don't like it.
I've already tried to do a simple if and else statement
if(productvm == null)
{
then
return RedirectToPage("Index")
}
else
return View("ProductPage", productVm);
but it didn't seem to hit the if statement
[Route("ProductPage/{id}")]
public IActionResult ProductPage(int id)
{
Product product = _uow.Products.SelectProduct(id);
var stockViewModels = new List<StockViewModel>();
foreach (Stock stock in product.Stock)
{
stockViewModels.Add(new StockViewModel()
{
Id = stock.Id,
Description = stock.Description,
IsAvailable = stock.IsAvailable,
Quantity = stock.Quantity,
});
}
ProductViewModel productVm = new ProductViewModel
{
Name = product.Name,
Id = product.Id,
Description = product.Description,
Price = product.Price,
Stocks = stockViewModels,
};
if (productVm == null)
{
return RedirectToPage("Productslist");
}
else
{
return View("ProductPage", productVm);
}
}
I basically want an error handling the controller if the id is not found in the database then execute XX
The way how I test the function is to change the ID when browsing the page with an ID that does not exist in the database, then I get this error:
https://i.imgur.com/1amWx43.png
and I want to handle it
I think your problem is that you have new the productVm object before the if, so it will never be null, for your case, you should get check the product object and not the productVm, for example:
Product product = _uow.Products.SelectProduct(id);
if (product == null)
{
return RedirectToPage("Productslist");
}
else
{
return View("ProductPage", productVm);
}

Concurrency exceptions when I tried to modifies two tables in MVC

I am having a concurrency Exception when I tried to update MessageTemplate and TemplateLookup table. MessageTemplate table update fine but the error occur when the code tried to update TemplateLookup table. Any help will be appreciated
Error
"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions."
Controller
public async Task<ActionResult> Edit (MessageTemplate messagetemplate,int Id, bool Enable, int id)
{
TemplateLookup template = new TemplateLookup();
var appdata = DateTime.Now;
try
{
if (ModelState.IsValid)
{
var currentUser = await _db.vwUsers.FirstOrDefaultAsync(i => i.DomainUserName == User.Identity.Name);
if (currentUser != null)
messagetemplate.LastUpdatedByUser = currentUser.FullName;
messagetemplate.LastUpdatedByUserID = User.Identity.Name;
messagetemplate.LastUpdatedOn = appdata;
//messagetemplate.Id = Id;
_db.Entry(messagetemplate).State = EntityState.Modified;
//await _db.SaveChangesAsync();
if (Enable == false)
{
var templateidlist = _db.TemplateLookups.Where(v => v.TemplateId == Id).ToList();
messagetemplate.Enable = Enable;
foreach (var templateid in templateidlist)
{
template.TemplateActive = Enable;
template.LastUpdatedOn = appdata;
template.LastUpdatedByUser = currentUser.FullName;
template.TemplateId = Id;
// Error occur here
_db.Entry(template).State = EntityState.Modified;
}
}
_db.Entry(messagetemplate).State = EntityState.Modified;
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(messagetemplate);
}
}
Well as it says - it looks like the row is being modified in between your query being started and actually saved...
I noticed you change the Id:
template.TemplateId = Id;
which you also search on;
var templateidlist = _db.TemplateLookups.Where(v => v.TemplateId == Id).ToList();
So if this action is run twice with the same template Ids it may change them before the second one has a chance to run.
Ususally in this concurrent modification scenario you'd catch the exception and re-display the form with the new values - but it depends on your usecase we can't say without more information about what your application does...
You can read about various options to handle it here: https://learn.microsoft.com/en-us/ef/ef6/saving/concurrency

MVC Session global variable

Here is what I'm trying to achieve. Certain options at the navbar should be available only if the user has "subordinates" in the database.
So, at the navbar I have:
The Approvals should be hidden for some users, but available to others. For those whom it should be available, the user must:
A) Be a Supervisor or,
B) Have a subornidate at the DB table
So, as for "A" it's pretty straightforward. I did:
#if (User.IsInRole("Supervisor"))
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
For "B", I was suggested to use Sessions. Well, great. So I came to the question: how can I make a single request to the DB and assign it to a Session["HasSubordinates"] so I can do this check?
#if (User.IsInRole("Supervisor") || (bool)Session["HasSubordinates"])
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
What I tried was to have:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
for every single controller, but that didn't worked well because sometimes I get null pointer and it looks absolutely rubbish.
I know it may sound like a trivial question for some (or most), but I'm really stuck and I do really appreciate any help.
Looking at your code, getting a user subordinates should only happen once. In your Login method:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
Create a new class to extend IPrincipal:
public class IPrincipalExtensions
{
public bool HasSubordinates(this IPrincipal user)
{
return Session != null && Session["HasSubordinates"] != null && Session["HasSubordinates"] > 0;
}
}
Now, in the View:
#if (User.IsInRole("Supervisor") || User.HasSubordinates() )
{
}
Writing from memory, may have left something out, but this should be the cleanest.
Don't use the session for this. What you need is a child action.
[ChildActionOnly]
public ActionResult Nav()
{
var model = new NavViewModel
{
IsSupervisor = User.IsInRole("Supervisor");
HasSubordinates = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
}
return ParialView("_Nav", model);
}
Then, just create a partial view, _Nav.cshtml and utilize the properties on the view model to render your nav however you like.
If you want, you can even use output caching on the child action, so it's only evaluated once per user. There's no built-in way to vary the cache by user, so first, you'll need to override the following method in Global.asax:
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
With that, you can then just decorate your child action with:
[OutputCache(Duration = 3600, VaryByCustom = "User")]

Keeping a page's state

I have already looked at these links for references.
Link 1: ASP.Net MVC and state - how to keep state between requests
Link 2: ASP.NET MVC: Keeping last page state
I have a few pages that a user will be filling out. We will call these pages Page 1. If they get to a field that they need to select from, drop down, but need to create a new item to be included in the drop down, because it will be used again later, they go to a new page, Page 2, to create the item. After create they create the item they are returned to Page 1 to finishing filling out the form. The problem is that the Page 1 is now erased because is a new page load. I would like for this to persist for when they come back so they don't have to refill out fields.
The route I am currently Link2 using a cookie. I don't know how to set the cookie's info before it gets to the next page, or how to pass it to that page before since it is going to a GET method and not a POST.
GET method for Page 1:
public ActionResult Create()
{
var courseTitles = (from title in db.CourseTitles
join type in db.CourseTypes on title.Type equals type.CourseTypeID
select new
{
CourseTitleID = title.CourseTitleID,
Title = title.Title + " - " + type.Type
});
Course course = new Course();
if (Request.Cookies["CourseInfo"] != null) //If it's not null, set the model.
{
HttpCookie cookie = Request.Cookies["CourseInfo"];
course.ClassNumber = Convert.ToInt32(cookie.Values["ClassNumber"]);
course.CourseStartDate = Convert.ToDateTime(cookie.Values["StartDate"]);
course.CourseEndDate = Convert.ToDateTime(cookie.Values["EndDate"]);
ViewBag.CourseList = new SelectList(courseTitles, "CourseTitleID", "Title", cookie.Values["CourseTitle"]);
return View(course);
}
ViewBag.CourseList = new SelectList(courseTitles, "CourseTitleID", "Title");
return View();
}
GET and POST method for Page 2:
public ActionResult NewCourseTitle()
{
ViewBag.Type = new SelectList(db.CourseTypes, "CourseTypeID", "Type");
return View();
}
//
//Post:
[HttpPost]
public ActionResult NewCourseTitle(CourseTitle courseTitle)
{
if (ModelState.IsValid)
{
db.CourseTitles.AddObject(courseTitle);
db.SaveChanges();
return RedirectToAction("Create", "Course");
}
return View();
}
Let me know if you need more code.
You can use TempData to store objects between requests:
public ActionResult Create()
{
var courseTitles = (from title in db.CourseTitles
join type in db.CourseTypes on title.Type equals type.CourseTypeID
select new
{
CourseTitleID = title.CourseTitleID,
Title = title.Title + " - " + type.Type
});
Course course = new Course();
if (TempData["CourseInfo"] != null) //If it's not null, set the model.
{
course = TempData["CourseInfo"] as Course;
ViewBag.CourseList = new SelectList(courseTitles, "CourseTitleID", "Title", course.Title);
return View(course);
}
ViewBag.CourseList = new SelectList(courseTitles, "CourseTitleID", "Title");
return View();
}
In order to store the Course simply use TempData["CourseInfo"] = course
TempData exposes couple of options that define for how long its content is going to be persisted. You can read about it here
You could use some JavaScript to modify the GET request to NewCourseTitle so that it will contain the course data that the user entered.
With jQuery it could look roughly like this:
$(function () {
var newCourseTitleLink = $('#new-course-title-link');
newCourseTitleLink.on("click", function ()
{
document.location.href = newCourseTitleLink.attr('href') + '?' + $('#course-data-form').serialize();
});
});
Then you can create a cookie in your action method NewCourseTitle:
public ActionResult NewCourseTitle(int classNumber, ... /*other form values*/)
{
var cookie = new HttpCookie("CourseInfo");
cookie.Values.Add("ClassNumber", classNumber.ToString());
...
Response.SetCookie(cookie);
ViewBag.Type = new SelectList(db.CourseTypes, "CourseTypeID", "Type");
return View();
}

Two checks IValidatableObject in one entity

Is the essence of Project, the creation of which is necessary to check whether there is already an entity with the same name. When editing needs such as checking, but keep in mind that the old and the new name of the entity can be matched.
You also need to display an error message. For this I use interface IValidatableObject, but do not know how to tell the Validate method the object is currently being edited or created
DbContext.ValidateEntity takes the IDictionary<Object, Object> items as the second parameter. You can pass any data there and the data you pass will be passed to IValidatableObject.Validate in the ValidationContext.Items
Assuming you refer to check EF cant do for you.
This is actually difficult to check. You are checking an entity after it has been added to the context. It should not check itself and needs to consider other items in context that are not yet saved. As well as the DB. There are several 3 combinations plus an self recognition. Record a an entity record in LOCAL when ID is blank/new ie multiple new inserts needs careful coding. (Consider using temp IDs)
the not yet saved entries should be in context
Context.Set<TPoco>().Local
and get data from DB and keep in a temp list. BUT dont put in context.
Or use a SECOND context.
var matchingSet = Context.Set<TPoco>().AsNoTracking() // not into context...
.Where(t=>t.field == somevalue).ToList();
So what about logical and actual duplicates on the DB. Logical duplicates are duplicates on a field with no unique index that from a business perspective should be unique.
If you want to check those...
You need to read the DB.... BUT if these records are currently being changed, you CAN NOT just put them into the Context. You would overwrite them.
But what if the values the logical key values have changed?
Something caused a logical dup on a record on the DB may no longer be a dup once saved or vice verse. Is that still a dup or not ?
So you need to decide how you match LOCAL versus loaded records.
Ie check LOCAL and matching DB records and decidr what to do if a record is in both, only local or only db.
LOCAL ONLY and DB Only is easy.
But in both... That is your business process decision.
Problem is solved using method ModelState.AddModelError (string, string) in actions Edit and Create.
[HttpPost]
[HandleError(View="AjaxError")]
public ActionResult Edit(ProjectsViewData data)
{
if (ModelState.IsValid)
{
if (!ContainsProject(data.CurrentObject.Name))
{
db.Projects.Attach(data.CurrentObject);
db.ObjectStateManager.ChangeObjectState(data.CurrentObject, EntityState.Modified);
db.SaveChanges();
return Projects(data);
}
else
{
int projectId = (from p in db.Projects
where p.Name == data.CurrentObject.Name
select p.ProjectID).FirstOrDefault();
if (projectId == data.CurrentObject.ProjectID)
{
db.Projects.Attach(data.CurrentObject);
db.ObjectStateManager.ChangeObjectState(data.CurrentObject, EntityState.Modified);
db.SaveChanges();
return Projects(data);
}
else
{
ModelState.AddModelError("Name", Localizer.ProjectAlreadyExists);
}
}
}
data.ObjectToEdit = data.CurrentObject;
return Projects(data);
}
[HttpPost]
[HandleError(View = "AjaxError")]
public ActionResult Create(ProjectsViewData data)
{
if (ModelState.IsValid)
{
if (!ContainsProject(data.CurrentObject.Name))
{
db.Projects.AddObject(data.CurrentObject);
db.SaveChanges();
return Projects(data);
}
else
{
ModelState.AddModelError("Name", Localizer.ProjectAlreadyExists);
}
}
data.ObjectToAdd = data.CurrentObject;
return Projects(data);
}
Helper method:
private bool ContainsProject(string projectName)
{
if (projectName != null)
{
projectName = Regex.Replace(projectName.Trim(), "\\s+", " ");
List<string> projects = new List<string>();
var projectNames = (from p in db.Projects
select p.Name.Trim()).ToList();
foreach (string p in projectNames)
{
projects.Add(Regex.Replace(p, "\\s+", " "));
}
if (projects.Contains(projectName))
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}

Resources