I’m working on a MVC 5 (asp) application. One of the requirements I get is to have multiple navigation paths to point at the same destination page. The problem is going back to a previous page after a post according to the navigation history of the user.
Let’s consider a basic scenario of three webpages
Customers/ShowAll -> Show a list of all customers
Customers /Search -> Show a list of customers according to a search (name, country …)
Customers /Update/1134 -> Show the update page for a specific customer (i.e. customer_id=1134)
So the navigation path goes like this
ShowAll -> Update
or
Search -> Update
If the user navigates to “Customers /Search” then “Customers /Update/1134”, updates the customer information and saves the data, I want the server to redirect to the page “Customers /Search” since it’s the path the user uses.
This is a very basic case but it can be more complex like going back many pages and always return a previously visited page (or a default one if no pages match the history).
What I’ve done so far
I have created a prototype that keeps track of the navigation history of a user on the server side. It uses session storage on the client side to give a unique id for the current browser tab. On each page unload it adds the browser tab id to the cookies. Then on the server side there’s a dictionary (in the session) with the tab id (extracted from the cookies) as the key and a list of visited URLs as the value. The current URL is added to the list of URLs. I found this solution to be working but it has some flaws.
If JavaScript is disabled this solution won’t work (this is not a very big deal since I can require all the users to turn it on (it’s an intranet for a small company))
If a tab is duplicated the resulting two tabs will have the same id. This is due to the implementation of session storage (at least on Chrome). So the history on the server can get corrupted if the user uses both tabs.
I store the dictionary of history in a session variable so if the session timed out, the history is lost. I thought about keeping the history in the database but I feel it’s a little of overheat for the database.
A last thing is that the dictionary of URLs is limited to the last 30 pages visited since I want to limit the server memory. It’s not important to my question but I feel to mention it since I’m sure some of you may see the problem of keep all pages from all users for all tabs in history.
I also thought of a similar solution using cookies to transmit the last 30 pages visited on each request and have the server parse this history when it needs it. Only the pages from the application domain will be kept. This will resolve the problem of persistence after a session times out but it introduce a little more processing to the server since the history will be parsed in about each request.
I want to know if there a better solution to redirect the user according to navigation history. Maybe there’s a build-in functionality in MVC 5 that I don’t know.
Thanks for any advice.
Regards.
Using session state (as you have discovered) is not a very good solution to this problem because:
It times out, in which case the data is lost.
If the user doesn't navigate to the page the way you expect (for example, coming directly to a page via Google SERP), then it doesn't work.
The only way to make it work 100% of the time is to put all of the navigation identifier information into the URL so the system can determine how to build the navigation links.
There is no built-in functionality for this in MVC 5, but you could use MvcSiteMapProvider to solve your issue. It contains HTML helpers for Menu and SiteMapPath, which acts like a breadcrumb trail.
#Html.MvcSiteMap().Menu()
#Html.MvcSiteMap().SiteMapPath()
It works on a different principle - it loads a single shared hierarchy of nodes (a site map) into memory. Then each request that comes in matches one of the nodes and uses the map to determine how to build the links in the HTML helpers. There is no session state used at all.
The trick to getting it to work in your scenario is to make the Customers/Update/1134 page available on 2 different URLs. Then you can configure 2 different node hierarchies and it will know which one to match based on the routing information.
For example, you could add an additional route value that indicates that the page you are navigating from is the search page.
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "Search" }, null)
By default, this will build a URL like Customers/Update/1134?source=Search. You can make it look prettier by adjusting your route configuration.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "SearchSource",
url: "Search/{controller}/{action}/{id}",
defaults: new { source = "Search", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ShowAllSource",
url: "ShowAll/{controller}/{action}/{id}",
defaults: new { source = "ShowAll", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Now with the same ActionLink shown above, you will get the URL /Search/Customers/Update/1134. That's better. Note that when you put the ActionLink on your ShowAll page, it should be like this instead:
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "ShowAll" }, null)
Then when you set up the node configuration in MvcSiteMapProvider, you need to make 2 different parent nodes, like this.
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<!-- Additional nodes here -->
<mvcSiteMapNode title="Search" controller="Customers" action="Search">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="Search" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Show All Customers" controller="Customers" action="ShowAll">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="ShowAll" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<!-- Additional nodes here -->
</mvcSiteMapNode>
You will then get a complete navigation solution:
/Customers/ShowAll | Home > Show All Customers
/ShowAll/Customers/Update/1134 | Home > Show All Customers > Update Customer
/Customers/Search | Home > Search
/Search/Customers/Update/1134 | Home > Search > Update Customer
Of course, this is just an example. You can make the URLs and navigation links look any way you want.
Redirecting Back
Finally, there is redirecting back to the location the user came from. That's easy because MvcSiteMapProvider keeps track of the parent node.
[HttpPost]
public ActionResult Update(CustomerModel model)
{
// Update customer here...
var currentNode = this.GetCurrentSiteMapNode();
if (currentNode != null)
{
var parentNode = currentNode.ParentNode;
if (parentNode != null)
{
return Redirect(parentNode.Url);
}
}
return View(model);
}
You may wish to store some additional information (sort order, search term, etc) from the original parent page, in which case you will need to pass those parameters through the Update page and back to the parent page somehow.
One way is to use session and then use some logical default behavior if they are missing when you get to the redirect page.
Another (easier) approach is to add them as parameters (query string or route values) to the URL of the Customer Update page so they will automatically be built into the return URL. Since you have 2 different routes, you would just need to add the information to the appropriate route. You just need to ensure both the routes of the Search page and the Customer Update page include them so the parent URL is built appropriately.
Full Disclosure: I am a major contributor of the MvcSiteMapProvider project.
See also:
https://github.com/maartenba/mvcsitemapprovider/wiki/Multiple-Navigation-Paths-to-a-Single-Page
http://www.shiningtreasures.com/post/2013/08/07/MvcSiteMapProvider-40-a-test-drive
http://www.shiningtreasures.com/post/2013/09/02/how-to-make-mvcsitemapprovider-remember-a-user-position
Related
I have a Link where I want to pass certain params in the URL but I don't want the browser to display the params.
I'm using Link's as for this:
<Link href={`/link?foo=bar`} as ={`/link`}>
<a>Link</a>
</Link>
But when I click this link and I try to access the params via router, I can't access foo=bar:
const router = useRouter()
console.log(router.query)
Returns
{
slug: ["link"],
}
And not
{
slug: ["link"],
foo: "bar",
}
So how can I access the URL params in href when using as for Link?
TL;DR You can't use as like that.
This is an incorrect usage of href and as. It would be cool if we could hide state from the end users to keep our URLs nice, clean, and compact, but obviously if you do that, you'll actually lose the state when copy/pasting the URL. That's why you can't hide query parameters in anyway (except for excluding them).
Here's the docs on href and as (dynamic routes, has little to do with hiding query params):
https://nextjs.org/docs/tag/v9.5.2/api-reference/next/link#dynamic-routes
And to further bring up my point, imagine if we could hide state, and we redirect to this URL:
https://example.com/stateful/
Presumably there would be some behind-the-scenes browser action that persists the state.
Now we copy/paste the URL:
https://example.com/stateful/
Oops! We don't have the state anymore because the browser has no previous state to keep track of! That's why you use query parameters, because they keep the state in the URL itself.
I have a list of employee list on EmployeesComponent and there is "Education Overview" and "Salary Overview" buttons for each records. When I click one of the overview button it goes to the OverviewComponent first and then load the correponding component (salary or education) into this OverviewComponent. There is also a "Back" button on each of these salary and education components. The structure is as shown on the following image:
components
The problem is that: When I come back to the EmployeesComponent, I need to reload the paging params e.g. the last page number before navigating to the overview pages. For this I use localStorage and check the saved value on each page load of the EmployeesComponent.
searchParams: any;
ngOnInit() {
let searchParams = JSON.parse(localStorage.getItem('routeParams'))?.searchParameters;
if(searchParams){
this.searchParams = searchParams;
window.localStorage.removeItem('routeParams'); // remove routeParams from localStorage
// load list using this.searchParams
}
But I save the page params on the OverviewComponent so that use a single place for salary and education pages. I think it is not a good approach and it may cause the localStorage items to be mixed as they use the same key (for some reason I need to use the same key sometimes).
So, should I set the paging parameters just before navigating to the overview page in the EmployeesComponent? And then check them on loading EmployeesComponent? What is a proper way for this scenario?
You can use the query-params in routing.
So now when you redirect from employess component to overViewComponent, then based on click i.e., Education Overview or Salary Overview just send the query params with the url.
Then now when you get back to employess component, just use the query params value you get in overView component and you can get the information you want back in employess component.
Q- what is the most proper place for adding and removing paging items to local storage
A- Most proper place for adding and removing localstorage item is where it get's change.
In your case, just set localstorage in overView component where you are getting params ( this.activateRoute.params() ) inside this function. And remove the localstorage on ngOnInit function of employee component.
I'm trying to create a custom glimpse plugin that shows us some information from our server.
When I open the page I get all the data I need but I want to be able to update this data once every 20 seconds(or when i click on a button in the tab) without having to refresh the entire page.
I've managed to add my JavaScript to the page and subscribe to the render and shown events, but i don't know how to update the tab content when something happens.
This is my tab
public class EagleTab : AspNetTab
{
private readonly IGlimpseInterventionService _glimpseInterventionService;
public EagleTab()
:this(new GlimpseInterventionService()){}
public EagleTab(IGlimpseInterventionService glimpseInterventionService)
{
_glimpseInterventionService = glimpseInterventionService;
}
public override object GetData(ITabContext context)
{
var interventionSection = new TabSection("Last modification", "Last Message","Time since last modification","CSE","DPS", "Start Date","End date");
var now = DateTime.Now;
var twoHoursThresshold = new TimeSpan(1,0,0);
foreach(var inv in _glimpseInterventionService.GetActiveInterventions()){
var timeSinceLastMod = now - inv.LastModification;
interventionSection.AddRow()
.Column(inv.LastModification)
.Column(inv.LastMessage)
.Column(timeSinceLastMod.ToString("c"))
.Column(inv.CSEName)
.Column(inv.DPSName)
.Column(inv.StartDate)
.Column(inv.EndDate).WarnIf(timeSinceLastMod<twoHoursThresshold);
}
var plugin = Plugin.Create("Section", "Data");
plugin.AddRow().Column("Active interventions").Column(interventionSection);
return plugin;
}
public override string Name
{
get { return "Eagle Tab"; }
}
}
And this is the JavaScript
(function ($, pubsub, tab, render) {
'use strict';
function refreshTabContent(){
//what am I supposed to do here
}
pubsub.subscribe('action.panel.rendering.eagle_glimpse_plugins_eagletab', function (args) {
});
pubsub.subscribe('action.panel.showed.eagle_glimpse_plugins_eagletab', function (args) {
setTimeout(refreshTabContent,30000);
});
})(jQueryGlimpse, glimpse.pubsub, glimpse.tab, glimpse.render);
As you can see in the js there is a function called refreshTab i want there to update the content of the tab.
I know that i could make an Ajax call to a controller of mine get the data and then try updating the panel using jQuery but that just seams a bit wrong and i'm hoping there's a better way.
Any tutorial or documentation about glimpse client side extensibility would be welcomed as well.
You have gotten a fairly large amount of the way towards your goal. Unfortunately though there isn't really a way of getting data from a tab outside of a request. That said, there is a "Glimpse" way of getting data from the server. This is small semantic difference, but it has to do with server data, vs request data.
If I was you, I would probably write this as a client side only tab and not implement AspNetTab. Here are some examples of how this can be done. Next I would implement a Resource. Unfortunately its not very well documented but fortunately its not very hard to work with.
This repo has some examples of how to work with client tabs back to resources. Specifically the Inventory tab is a tab that lets people mouse over products in the site and have the tab show stock levels. Here is the resource and here is the client code that interacts with the resource (given what you have so far, this should be fairly easy to adapt. Lastly, as a bonus, if you haven't seen it already, here is how to include your script into your page. Note, the commits on that repo are the step by step guid to how things come together.
Let me know if that helps.
I'm reading David Sulc's A gentle introduction to Maionette, and came across the following:
It’s important to note that the route-handling code should get
fired only when a user enters the application by a URL, not each time the
URL changes. Put another way, once a user is within our Marionette app,
the route-handling shouldn’t be executed again, even when the user
navigates around;
What's the problem with triggering a handler on navigate?
There is no difference IF you aren't already in your Marionette app. So say we are first getting into our Marionette app and we want it to initially route to the posts index page. Initially we can either
call navigate({trigger: true) or
call navigate (to update the URL) and then call App.vent to trigger the call.
Both of them will resolve in our controller's API.list function and behave exactly the same way (fetch our list of posts and then display it). So calling trigger: true when initially entering your app/routing to the first page is totally fine. I think David just tries to make it a practice to not do so to re-enforce the power of Marionette's pub/sub infastructure since with it you don't need to pass trigger: true.
However, let's say we're now in the list view displaying a list of posts. We've already spent the time of fetching our list of posts from the server when initially entering our app. Now we click on a post and want to view the show view of that post. The post already exists in memory so we can just do a App.vent.trigger "post:clicked", post to use the post already in memory to display it. If we were to instead utilize the navigate({trigger: true}) route instead we'd end up on the same page but we would have to re-fetch the individual post instead using the one already in memory.
So the main reason is because you don't need to - triggering the page would cause a reload, re-fetch, etc. It would make your app feel slow and kind of defeat the purpose of a responsive web app/single page application.
Here's what your router should look like - you always want it setup so that you can just navigate to the page via a App.vent call when inside your app AND able to handle the manual browser refresh/navigating to the route directly (which is what the trigger would do, but this is the slow load that you'd kind of expect when initially fetching resources/entering the application. When in your app you want it to be the fast responsive piece that the pub/sub infrastructure affords).
#SampleApp.module "PostsApp", (PostsApp, App, Backbone, Marionette, $, _) ->
class PostsApp.Router extends Marionette.AppRouter
appRoutes:
"" : "list"
":id" : "show"
API =
list: ->
new PostsApp.List.Controller
show: (id, post) ->
new PostsApp.Show.Controller
id: id
post: post
App.vent.on "posts:list:clicked", ->
App.navigate "/"
API.list()
App.vent.on "post:clicked", (post) ->
App.navigate "/" + post.id
API.show post.id, post
App.addInitializer ->
new PostsApp.Router
controller: API
Then to navigate there you'd just call App.vent.trigger "posts:list:clicked" from wherever you want (like after clicking a "View all posts" button and bubbling the event up to the controller and active on that event).
#listenTo bannerView, "posts:list:button:clicked", (args) ->
model = args.model
App.vent.trigger "posts:list:clicked"
EDIT:
In the controller handling the show call to avoid the re-fetch:
#SampleApp.module "PostsApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ post, id } = options
post or= App.request "post:entity", id
App.execute "when:fetched", post, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion post
#postRegion post
#bannerRegion post
#show #layout
Lots of time spent with other language/templating-engine combinations, however fairly new to JSF and have the "pleasure" of taking over a legacy JSF 1.1 project that uses a basic Sun JSF lib and for political reasons can not be updated to something more modern like JSF 2.2 with PrimeFaces or IceFaces, which I'd personally like to do after reading up on the capabilities of those.
The setup is a simple web app with landing page plus two tabs (Person & Address), each powered by their own Backing Beans and JSF pages. Six total navigation states as each tab starts with a "Search Form" page which leads to a "Search Results" page on submit and then a "View" page onmousedown (click action) of an individual result; for each of Person & Address data. The "Search Form" pages are session-scoped, and designed to pass search parameters around to each other in a header box entitled "User's Search Query". It does this because the "Search Results" page for Address searches is really an overloaded Person search result with Address thrown in for "Search for People by Address". Yes this could all probably be accomplished from a single page with some optional search params, but its not, and for whatever reason I'm not permitted to change the core page structure, just tasked to bug fix.
There's a long-standing bug I've been asked to fix, where one page (Person) always shows search parameters from the other (Address) when both are set, because of some incomplete logic (inside the panelGrid where the search query parameters are displayed):
<h:outputText value="Coverage Type:"/>
<h:outputText value="#{addressSearchUI.searchParameters.coverageType}"
rendered="#{empty personSearchUI.searchParameters.coverageType}"/>
<h:outputText value="#{personSearchUI.searchParameters.coverageType}"
rendered="#{!empty personSearchUI.searchParameters.coverageType}"/>
A little too overly simplistic; if a Person search's Coverage Type was set, use that, but if not, try to use the Address coverageType search parameter. Works great when only one or the other were set.
PROBLEM CASES:
Since they are session scoped, what if both of them were set in separate searches and we're on the Address page? The value from the old Person search displays (and vice versa; this is the main problem). What if neither of them are set yet? It should show a default value of ALL COVERAGE, easy enough to add but doesn't address that main problem.
SOLUTION:
Thought this would be an easy fix but I've hit a major wall trying to do either one of the following two simple things that I think would solve my problem:
A) get the session-scoped value of one page's search parameter and reset it or set to empty string (then that naiive presentation logic could be left as is)
B) call the reset button of one page programmatically from Java when backing bean init() method gets called as we switch tabs (I can call the method but am not sure how to get the original session instance rather than creating a new one, which will fire to a non-existent page UI-wise)
Here's what I've tried so far:
A) Adding an actionListener attribute to the submit button itself and trying to forcibly reset values from the other Search Form.
<h:commandButton
id="personReset"
styleClass="submitButton"
value="Reset"
action="#{transactionSearchUI.resetSearch}"/>
<h:commandButton
id="personSearch"
styleClass="searchButton"
value="Search"
rendered="#{personSearchUI.canDo['performSearch']}"
actionListener="#{addressSearchUI.resetSearch}"
action="#{personSearchUI.performSearch}"/>
B) In the backing bean of each search, try to invoke the reset button of the other:
/* utility method */
private UIComponent findComponent(String id, UIComponent where) {
if (where == null) {
return null;
}
else if (where.getId().equals(id)) {
return where;
}
else {
List<UIComponent> childrenList = where.getChildren();
if (childrenList == null || childrenList.isEmpty()) {
return null;
}
for (UIComponent child : childrenList) {
UIComponent result = null;
result = findComponent(id, child);
if(result != null) {
return result;
}
}
return null;
}
}
//... then inside init() method that is called when Person tab loads
FacesContext facesContext = FacesContext.getCurrentInstance();
UIViewRoot root = facesContext.getViewRoot();
HtmlCommandButton button = (HtmlCommandButton) findComponent("addressReset", root);
ActionEvent actionEvent = new ActionEvent(button);
actionEvent.queue();
Fails with NullPointerException, so clearly can't access one tab from the other's backing bean in this way (unless I'm missing something).
C) JavaScript approach to reset values from one tab to the other:
<script type="text/javascript">
function clearAddressSearchParams() {
document.getElementById('AddressSearch:addressSearch:addressReset').click();
document.forms['AddressSearch:addressSearch'].reset();
return false;
}
window.onload = clearStats;
</script>
Neither of those client-side methods of trying to fire the "Reset" button action are going to work, since again, the "Person" and "Address" Search Forms were setup as two totally separate beans and pages (even though one ultimately extends the other), one won't be active (or available in the DOM) so it won't find the elements.
At this point, I know there is clearly a simpler solution, but I'm at my wits end and probably coming at this from the wrong angle, so I don't know what else to do but put up a bounty for some of the JSF geniuses here. But if anyone can suggest a fix (given my limitations of older JSF technology, no adding external libs and maintaining same basic code structure), you'll get the bounty and tons of appreciation.
EDIT: To summarize all this into a single question, can anyone suggest a reliable (legacy-JSF 1.1 compatible) way to flush a session variable or reset it, independently of its page where it gets set being active?