How to return to calling page, but with a slight difference, in MVC3? - asp.net-mvc

I need to call another MVC3 page(P2) and then return to calling page(P1). However the slight twist is that P2 needs to call itself, so the referrer can end up being P2.
So:
P1 - (P2 -> P2 -> P2) ->P1
So the question is how do I get P1's referrer URL and keep it and then use it later to go back to P1, regardless of the number of time P2 calls itself.
I did try and populate ViewBag.Referrer:
Back
using the following controller code, trying to only set it on the original call. However ViewBag.Referrer always seemed to pick up the P2 Referrer URL, even though in debug mode it was not resetting ViewBag.Referrer due to IsOriginalCall=0. It is weird. It is as if I am storing a pointer and not the value.
public ViewResult Index(int id = 0, int IsOriginalCall = 0)
{
if (IsOriginalCall =1)
{
if (Request.UrlReferrer != null)
{
ViewBag.Referrer = Request.UrlReferrer.LocalPath;
}
}
ViewBag.SLIid = id == 0 ? 4 : id;
return View(db.StdSection.Where(r=>r.InWizard).OrderBy(r=>r.Name).ToList());
}
Thoughts and a solution would be hugely appreciated. I have been going around in circles on this one.
Thanks in advance.
EDIT, Attempt 2 with TempData:
Calling code:
#Html.ActionLink("Sections", "Index","SSLI2", new { id=item.Id, ReturnUrl = Request.Url.ToString() },null)
Controller:
public ViewResult Index(string ReturnUrl, int id = 0)
{
if (ReturnUrl != "x")
{
//ViewBag.Referrer = Request.UrlReferrer.LocalPath;
TempData["Referrer"] = ReturnUrl;
}
ViewBag.SLIid = id == 0 ? 4 : id;
return View(db.StdSection.Where(r=>r.InWizard).OrderBy(r=>r.Name).ToList());
}
View:
Back
Which produces:
Back when P2 goes back to P2, but seems to use P2 referrer URL ????

You could pass the ReferrerUrl in from P1 querystring and then store the value in TempData. It will then survive transition from one action call to another.
Another option would be to pass the ReferrerUrl in from P1 in the querystring and then put the value in a hidden input on P2.
#Html.HiddenFor(m => m.ReferrerUrl)
or
#Html.Hidden("ReferrerUrl", ViewBag.ReferrerUrl)
then pick the value up on each post-back and render it as a hidden input value again.
Edit
You could maybe try doing something like:
public ViewResult Index(string returnUrl, int id = 0)
{
ViewBag.ReturnUrl = returnUrl;
ViewBag.SLIid = id == 0 ? 4 : id;
// This can be picked up in another action method.
TempData["ReturnUrl"] = returnUrl;
return View(db.StdSection.Where(r=>r.InWizard).OrderBy(r=>r.Name).ToList());
}
Then maybe render it using:
Back

Related

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();
}

View/Model data isn't refreshing/changing after post/postback, even though I'm using the PRG pattern

Update I have saved my problem a long time ago. The problem was that I was trying to call the view model on the wrong view method! I was calling the base view method (Document), instead of one of it's derived method (like NewDocument, PDFDocument, etc.) Thus it was only giving me the Documents data, which didn't change. I was looking and using the wrong view method all the time... Stephen, when you asked me
"Why do you create derived classes in a method but then return only the base class"
I couldn't answer the question at the time because I didn't even know myself, until I remember that originally, the method wasn't returning the base class. I only changed it so that it can work with the base view method, which was wrong in the first place!
That's what I get for only getting 3-4 hours of sleep in 3 days. Everything works right now. Thanks.
I'm having a hard time trying to figure out why the data in my view isn't changing after I do a post. Originally I was doing it via return View() and it worked, but since it was a partial view, the page didn't look great, so I was reading up and saw that it was better to do it by Post-Redirect-Get pattern (PRG) and to use an id value to retrieve the values instead of sending the entire model via Tempdata. I even used ModelState.Clear() and that didn't even work. When I debugged the code, the model only has the values from when I first called it.
Here's part of my Get controller:
NewDocument Get Controller
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// This doesn't work. The view keeps on showing the data from View(Services.CreateNewDocument()).
if (id != null)
{
return View(Services.GetdocumentViewModelData(DocEnum.Section.NEW_DOC_INDEX, (int)id));
}
// This works fine
return View(Services.CreateNewDocument());
}
And here's the post that calls the redirect:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
int? DocID = Services.AddingNewDocument(dvm);
// See, I even tried to clear it.
ModelState.Clear();
return base.RedirectToAction("NewDocument", new { id = DocID });
}
else
{
// Display errors in the modal
}
return base.RedirectToAction("NewDocument");
}
And here's the old way I did it:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
Services.AddingNewDocument(ref dvm);
dvm.NewRecordMode = DocEnum.Action.UPDATE;
// It worked, but only the partial view showed, and not the entire view.
return PartialView("_NewDocument", dvm);
}
else
{
// Display errors in the model
}
return base.RedirectToAction("NewDocument");
}
Could it be because I'm using a custom model binding?
My Custom Model Binding
public class BaseClassModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var modelType = bindingContext.ModelType;
var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ViewModel");
if (modelTypeValue == null)
throw new Exception("View does not contain the needed derived model type name");
var modelTypeName = modelTypeValue.AttemptedValue;
var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);
if (type == null)
{
throw new Exception(String.Format("Derived model type {0} not found", modelTypeName));
}
var instance = bindingContext.Model ?? base.CreateModel(controllerContext, bindingContext, type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
return base.BindModel(controllerContext, bindingContext);
}
}
EDIT: And here's the GetDocumentViewModelData code:
GetDocumentFromViewModelData
public static DocumentViewModel GetDocumentViewModelData(DocEnum.Section docType, int id)
{
switch (docType)
{
case DocEnum.Section.NEW_DOCUMENT_INDEX:
// NewDocumentTypeViewModel is a child to DocumentTypeViewModel
DocumentTypeViewModel nd = NewDocumentService.GetViewModelByID(id);
return nd;
case DocEnum.Section.PDF_DOCUMENT:
DocumentTypeViewModel pdfvm = PDFDocumentService.GetViewModelByID(id);
return pdfvm;
case DocEnum.Section.XLS_DOCUMENT:
DocumentTypeViewModel xlsvm = XLSDocumentService.GetViewModelByID(id);
return xlsvm;
}
return null;
}
Edit: Also adding the GetViewModelByID function
GetViewModelByID
public static DocumentTypeViewModel GetViewModelByID(int id)
{
docEntities db = new docEntities();
NewDocumentTypeViewModel vm = new NewDocumentTypeViewModel();
// Calls a stored procedure called Select_Documents_ByID(id) to get the note entry
// that was submitted.
List<Select_Documents_ByID_Result> prevNotes = db.Select_Documents_ByID(id).ToList();
StringBuilder sNotes = new StringBuilder();
foreach (var note in prevNotes)
{
sNotes.AppendFormat("{0} - {1}: {2}\n\n", note.CreatedDate.ToString("yyyy-MM-dd HH:mm"), note.username, note.Entry);
}
vm.PreviousNotes = sNotes.ToString();
return vm;
}
Edit: I did a direct creation of the view model inside the Get controller, and it's the same result. when i debugged the view itself, the values from the new view model don't show up. Instead, the values from the initial view model, View(Services.CreateNewDocument()), shows.
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// Right here I created the view model to test thing, but I'm getting the same results. Nothing has changed.
if (id != null)
{
var d = new NewDocumentTypeViewModel(1, "Help!");
// This property is from the base class, DocumentTypeViewModel
d.DocumentTitle = "Testing!";
return View(d);
// Inside the view itself, none of the values in the view model, including the one
// belonging to the base class. It still shows the initial values.
}
// This works fine
// Or maybe not...
return View(Services.CreateNewDocument());
}
Edit: I wanted to see if it was also doing the same thing for the initial call to the view return View(Services.CreateNewDocument()), and decided to change the value for documentTitle in the base class from New Document to a randomly-generated number, after the object has been created.
Here's the code for DocumentTypeViewModel's default constructor:
public DocumentTypeViewModel()
{
DocumentTitle = "New Document";
NewRecordMode = DocEnum.Action.ADD;
DocumentID = 0;
}
And here's the Services.CreateNewDocument() code where I change the DocumentTitle after the View Model has been created.
public DocumentTypeViewModel CreateNewDocument()
{
DocumentTypeViewModel dtvm = new DocumentTypeViewModel();
Random r = new Random();
dtvm.DocumentTitle = r.Next(5, Int32.MaxValue).ToString();
return dtvm;
}
Now in the View, when I call DocumentTitle:
<div class="label-text-group">
#Html.LabelFor(model => model.DocumentTitle)
#Html.EditorFor(model => model.DocumentTitle)
</div>
You would expect to see a randomly-generated number every time the View gets called. Nope, what you would see is "New Document". Weird.
It's seems that Services.GetDocumentViewModelData() is not exactly working correctly. It only carries the values created by the base class' constructor when a view is created, not any values that have been added or changed within GetDocumentViewModelData() itself. Why is that? What's going on? Please help anybody!
I have solved it. Look at the Update section on top. Thanks Stephen.

Routing with parameter conflicts issue

MY route config :
routes.MapRoute(
"LastTwoRoute",
"thong-ke-ket-qua-xo-so-2-so-cuoi/{cityID}/{pnumbers}/{pdays}/{ponlySpecial}",
new { controller = "LastTwo", action = "Index",
cityID = "MB",
pnumbers = "",
pdays = 1000,
ponlySpecial = false
});
The controller :
[HttpGet]
public ActionResult Index(string cityID, string pnumbers, int pdays, bool ponlySpecial)
{
[HttpGet]
public ActionResult Index(string cityID, string pnumbers, int pdays, bool ponlySpecial)
{
LastTwoParameters lastTwoParameters = new LastTwoParameters();
lastTwoParameters.listCities = Common.GetDropDownCitiesList();
lastTwoParameters.Numbers = pnumbers;
lastTwoParameters.Days = pdays;
lastTwoParameters.OnlySpecial = ponlySpecial;
lastTwoParameters.listLastTwoResult = new List<getReport_LastTwo_Result>();
if (TempData["Redirection"] != null || !string.IsNullOrEmpty(pnumbers) )
{
if (!string.IsNullOrEmpty(cityID) && pdays > 0)
{
using (KQXS context = new KQXS())
{
lastTwoParameters.listLastTwoResult = context.getReport_LastTwo(cityID, pnumbers, pdays, ponlySpecial).ToList();
}
}
}
return View(lastTwoParameters);
}
[HttpPost]//Run action method on form submission
public ActionResult Index(List<Cities> c, string cityID, string numbers, int days, bool onlySpecial)
{
TempData["Redirection"] = true;
return RedirectToRoute("LastTwoRoute", new {
cityID = (string.IsNullOrEmpty(cityID) ? "MB" : cityID ),
pnumbers = (string.IsNullOrEmpty(numbers) ? string.Empty : numbers) ,
pdays = (days == 0 ? 1000 : days),
ponlySpecial = onlySpecial});
}
When I frist access the controller :
and hit the submit button without entering/modifying any parameter, there are no errors :
but if I modify the third or the fourth parameter, I will have this error :
No route in the route table matches the supplied values.
I debuged the code, and at the line RedirectToRoute in HttpPost, every parameters are about the same except the parameter that I modified. I can't think of a reason why is this error happening!
If I enter/modified the second parameter (the second text box counting from top to bottom), I have no errors either!
Any help is greatly appreciated!
P/s : If this is not clarify enough for you because of my poor English, I can provide a screen video which records how I get the error!
You have pnumbers = "" in your route but it's not marked as an optional field (and you wouldn't be able to have it as optional if it's in the middle with required fields around it).
Try defaulting it to "0" or something.
Another alternative is to move this option to the end of the required parameters and mark it as optional like:
pnumbers = UrlParameter.Optional
It's worth installing route debugger if you are having routing issues as it adds a nice interface at the bottom of the page which shows which routes will trigger and which wont. It's essential with complex routes IMO.

Redirecting in ASP.NET MVC with parameters

I have the following code but it doesn't seem to redirect to my action with the given parameter. I have redirected something similar but the parameters were query string parameters. I'm wondering if it is done another way for parameters since the following doesn't work or what I might be doing wrong in my call to the action?
public ActionResult PassThrough (long i)
{
return RedirectToAction("RedirectAction", new { d = i});
}
public ActionResult RedirectAction (long d)
{
return SomeView();
}
You need to return the redirection command as result:
public ActionResult PassThrough (long i)
{
return RedirectToAction("RedirectAction", new { d = i});
}

Resources