router
router copied to clipboard
View and ViewModel caching option in router
Currently, the aurelia router always re-create ViewModel and View when navigated. There doesn't seem to be an option in the router to cache the ViewModel and its associated View, avoiding them to be re-created in subsequent navigation. The benefit of caching is obvious: better user experience. It's also a common implementation used by top commercial apps, including iCloud.
Regarding experience, let's use iCloud as reference. Goto Mail, scroll down a few times, select a message. Now navigate to Contacts. Go back to Mail again, notice that everything is beautifully persisted. Aurelia router already have powerful capabilities, so I'd like to see caching options available, so achieving such experience will be a piece of cake.
Regarding the implementation, that won't be too hard. During swap, instead of replacing the old DOM with new one, we can store the old DOM and the respective ViewModel to a hidden container, or perhaps in-memory array. In subsequent navigation, if the View/VM already available, simply pick it up. In this case, I expect only activate cycle will be called, allowing me to perform additional operation in "re-entrance".
+1 it's tedious to recreate viewstate by code all the time i've commented already on https://github.com/aurelia/router/issues/170 basically i would like to see a "cache" parameter on the route config
Agree. The navigation caching is a standard feature in most app framework. This should be a top priority for Aurelia team, quoting the aurelia mission to be "strongly focused on developer experience".
It is planned. If you want, you can implement it now yourself. What you would need to do is create your own version of the router-view element that caches and re-uses view instances. We will build that in though.
Sounds good. I'll prefer to wait then, while I still need more time to familiarize with other Aurelia concepts. It'll be great to see this implemented before beta though. Thanks!
@jimmyps Note that half of this is readily available. Put @singleton(false) on your viewmodel and it's re-used.
This at least persists and restores your VM state, but the other half (View reuse) is stil required because not all the UI state is in your VM... I give sample use cases in #21
Thanks @jods4 for the update. Since the view caching is my primary concern, I'll be looking forward to it and give it a spin once it's available. Is it currently in progress? Any ETA yet?
With respect to the view cache...no "official" work has been done yet. However, as part of some work for a customer, I built a floating window system on top of the router. So, when routes were re-visited it would use the existing view and view model (and simply bring the window to front) rather than re-create them. That work will help in implementing the router-view's caching implementation.
There are a few other high priority issues that need to come first, but this is still planned and that bit of very specific work has some general code that will likely make implementing this much faster.
Thanks for the insight @EisenbergEffect. No worries, just take your time to proceed with other higher priorities for now. It'll be great if the view cache can make its way sometime after the beta, or possibly before the RTM.
Will some form of offline (aka web storage) technology be implemented?
just loud thinking: maybe we can use browser history instead of view restoring
history.go(number|URL)
Any progress on this @EisenbergEffect ? I'm trying to build an "admin" style application. There's a main router on "app" linking to "login", "register" and "shell". "shell" in turn has a child router, with all the application's menu items in the sidebar (think WordPress's admin app). I'm desperately trying to find a way to reuse the Views and ViewModels inside the child router, its where the complexity and UX lies. I also attempted to eliminate the child router and use two view ports, but to be able to utilize this approach, I would need to only link to either the link on the "app"'s
You can implement it yourself. There's no need to wait on us. The router-view can be replaced with your own custom element. If you can't wait for us, I recommend you copy/paste the source code of our router-view into your own project, rename the custom element and implement the caching behavior that you desire. The router knows how to communicate with any custom element that registers itself as a view port with the router and that implements the proper interface. It was designed this way so that developers could extend the system in this way.
Ok, thanks will take a look at router-view and give it a go.
Just to add my 5 cents on this topic I'd expect a similar behavior than in xaml PRISM framework. Each UI region (a router view in aurelia) keeps a list of View/ViewModels that have been activated. Each time a view is navigated to it checks if any of the cached views has the same ID (route). If so, it calls a method on the view/view-model canNavigateTo, this way every view/view-model can decide if it can be recycled or a new instance must be created. In addition, PRISM implements a mechanism to handle view life time in order to control when views must be removed from the region's cache.
I like this way of handling views although I suspect it's no as easy in html as is in xaml to keep views alive in memory disconnected of the visual tree.
a couple more cents to add :smile:
@Alxandr gave me a solution a long time ago:
- add a custom element, resource-pooled to the view's template
- in the resource-pooled custom element, implement bind and unbind methods
- in the resource-pool service, implement get and free methods
ping @EisenbergEffect
@cmichaelgraham I think the issue here is not Resource pooling but View caching. At least I understood it differently. Notice how the OP wrote:
Go back to Mail again, notice that everything is beautifully persisted
Currently the problem with back navigation in Aurelia is that there's no way to restore any state that is not bound to a singleton ViewModel. Binding all state is tedious and useless. Typical state that you would not bind but can expect to persist: scrollbar positions, active tab in a tabcontrol, maybe focus, etc.
I see that your code uses the view url as pool name, which should make it unique and implies you will get back the same (===) view instance when you get back. A few comments on that design:
- Using a resource pool to cache singletons is a waste of resources... you don't need to store a dictionary of lists if you're only going to store a single instance.
- If you expose a generic Resource pool kind of API, it would be nice if it was a little more fully featured. In particular a way to decrease pool sizes (e.g. back pressure or MRU) would be nice.
- Reusing the view instance is required, but I'm not sure if it is sufficient to solve the problem. From what I see in your code, all lifecycle events will run when the view is unloaded (resp. loaded again). Isn't doing that going to destroy UI state?
PS: that said, view pooling is an excellent tool to have at hand, esp. in virtualized lists...
I'd just like to note that the resource-pool mentioned was done by me as a proof of concept. It has basically no features, and was just a test to see mainly if it could be done.
[Edit]
Or at least the original one was.
I should have mentioned that @Alxandr work was a Poc. Also @jods4 I appreciate your input. I may have misunderstood the original ask.
i'm trying to figure out a clean way to make this globe from esri maintain its state when you navigate away and back. it definitely falls outside the normal view case...
http://cmichaelgraham.github.io/skel-nav-esri4-vs-ts/index-release.html#/esri-globe
@cmichaelgraham I don't know how you did it, but it seems to work ;) When I go back the globe is at the exact same position!
I think using 3rd party plugins (e.g. jQuery plugins) that were wrapped into a custom attribute or custom element is the perfect example where you don't control the complete UI state and need a very special solution if you want to restore the view exactly as it was before.
I need multiple instances of the same viewModel class to be active at the same time (I have a tree view on the left side of the screen and as I click the node in the treeview the right side is populated with data. Every node has it's own instance of viewModel) I copied routerView element and changed process method to read the viewModel from cache (based on the route fragment). This works but it feels like it's the wrong place to do it because aurelia creates the viewModel somewhere before the process method and then if I have a viewModel in the cache I discard the created view model. (This works because I have registered my view models as transient). I would like to read my cache (based on the route fragment) before Aurelia creates the view model and if the view model exists in the cache pass that down the pipeline. Is that even possible? Were is the best place to do it? And I would like to skip CanActivate and Activate pipline steps if the viewModel is found in cache if it's possible...
You would need to implement your own RouteLoader as well. It's another extensibility point. You can look at the default implementation. It's also in the templating-router repo. Once you implement your own, you would need to register it in the root DI container. aurelia.registerSingleton(RouteLoader, CustomRouteLoader)
Thank you. It works great. I really enjoy working with Aurelia. Is there any way to skip Activate pipeline step. I have tried implementing determineActivationStrategy in my VM like this
determineActivationStrategy() {
return activationStrategy.noChange;
}
but that method was never called. So I looked through the source code and found that this always overrides my strategy (in router, class BuildNavigationPlanStep)
if (prevViewPortInstruction.moduleId !== nextViewPortConfig.moduleId) {
viewPortPlan.strategy = activationStrategy.replace;
Is there any way I could skip activate method on VM? The idea is to call activate method once the VM is first instantiated, and skip it if the VM is found in cache.
My current solution is a bit of a hack. If the VM is found in the cache I override my activate method. Something like
if (myViewModel) {
myViewModel.activate = () => {
return true;
}
You can always implement a simpel isActivated boolean value guard internally.
@EisenbergEffect In the same vein of what @rborosak asked, but opposite: Is there a way to instantiate a fresh VM when navigating to the current route again? Is there even a way to detect that? Scenario is a "New item" form that gets invoked from a menu entry. When clicking the shortcut again, it would be nice if the current form was dismissed and a brand new one shows up. Obviously the route doesn't change...
Yes, 7use determineActivationStrategy There should be notes on that in the cheat sheet.
I have exactly the same question / need as @jods4. I've got a "users" route (list of users) and a "users/add" route (to add a user). If I add a user and navigate back to the list and then navigate to "users/add" again, the previous values are populated.
I've added the determineActivationStrategy() as suggested in the cheat sheet, but that never fires on my "users/add" VM. Feels like there's something fundamental that is wrong or that I'm interpreting / implementing incorrectly.
Could this be as a result of the view being cached and its "bound" values are pulling through to the VM? Or is the VM (as well as the V) possibly cached in a way that I'm not aware of? I've played around with @transient as well, if I add it to the "users/add" VM, the route never fires.
@stefan505 You have something else going wrong. If you navigate back and then you navigate to a new route, you will get new instances unless you changed something somewhere explicitly. I'm going to take a wild guess here.. are you trying to inject User into your VM? If so, that's getting registered as a singelton and you are getting the same user instance even though you are getting a different view model instance. In general, you don't want to use DI with model objects. That's just a guess. I'd need a lot more info to figure out what your issue is.
@EisenbergEffect, you are right, something else is going wrong, but I found the problem.
I'm navigating from "users" to "users/add", then back to "users" then back "users/add" again. After testing all DI components one-by-one (i.e. one at a time trial-and-error), I found the error within aurelia-validation:
@ensure(function (it: ValidationGroup) { it.isNotEmpty().isEmail() }) <------------
public email: string;
The @ensure decorator is somehow causing my VM data to be "cached" incorrectly. If I Uncomment / remove that, I get a new instance every time as expected. Note, I was participating this thread: https://github.com/aurelia/validation/issues/216 where I'm using validationGroup = validation.on(MyClass.prototype) to get the property decorators to work.
@PWKad I suspect that something strange is going on with the way metadata and instance values are stored. I think @stefan505 may be right about the connection to the other issue.
@EisenbergEffect How would you put the cache on the history, in order to replicate the behavior of a Navigation View in iOS or Android? New view going forward, popping the view cache going backward.
I may be wrong but it seems you don't have that kind of info in the router-loader.loadRoute() nor the route-view.process().
Are those kind of features already planned? You sure need those for Aurelia-Interface.