Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View and ViewModel caching option in router #173

Open
Tracked by #239
jimmyps opened this issue Aug 4, 2015 · 62 comments
Open
Tracked by #239

View and ViewModel caching option in router #173

jimmyps opened this issue Aug 4, 2015 · 62 comments

Comments

@jimmyps
Copy link

jimmyps commented Aug 4, 2015

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

@fopsdev
Copy link

fopsdev commented Aug 4, 2015

+1
it's tedious to recreate viewstate by code all the time
i've commented already on #170
basically i would like to see a "cache" parameter on the route config

@jimmyps
Copy link
Author

jimmyps commented Aug 4, 2015

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

@EisenbergEffect
Copy link
Contributor

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.

@jimmyps
Copy link
Author

jimmyps commented Aug 4, 2015

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!

@jods4
Copy link
Contributor

jods4 commented Aug 19, 2015

@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

@jimmyps
Copy link
Author

jimmyps commented Aug 20, 2015

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?

@EisenbergEffect
Copy link
Contributor

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.

@jimmyps
Copy link
Author

jimmyps commented Aug 21, 2015

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.

@jimmywarting
Copy link

Will some form of offline (aka web storage) technology be implemented?

@fopsdev
Copy link

fopsdev commented Nov 18, 2015

just loud thinking:
maybe we can use browser history instead of view restoring

history.go(number|URL)

@stefan505
Copy link

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 , or the "shell"s . Aurelia doesn't allow this, it seems I have to use both for each route.

@EisenbergEffect
Copy link
Contributor

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.

@stefan505
Copy link

Ok, thanks will take a look at router-view and give it a go.

@dpinart
Copy link

dpinart commented Nov 28, 2015

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.

@cmichaelgraham
Copy link
Contributor

a couple more cents to add 😄

@Alxandr gave me a solution a long time ago:

ping @EisenbergEffect

@jods4
Copy link
Contributor

jods4 commented Dec 27, 2015

@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...

@Alxandr
Copy link
Contributor

Alxandr commented Dec 28, 2015

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.

@cmichaelgraham
Copy link
Contributor

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

@jods4
Copy link
Contributor

jods4 commented Jan 2, 2016

@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.

@rborosak
Copy link

rborosak commented Feb 7, 2016

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...

@EisenbergEffect
Copy link
Contributor

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)

@rborosak
Copy link

rborosak commented Feb 8, 2016

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

@EisenbergEffect
Copy link
Contributor

You can always implement a simpel isActivated boolean value guard internally.

@jods4
Copy link
Contributor

jods4 commented Feb 9, 2016

@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...

@EisenbergEffect
Copy link
Contributor

Yes, 7use determineActivationStrategy There should be notes on that in the cheat sheet.

@stefan505
Copy link

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.

@EisenbergEffect
Copy link
Contributor

@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
Copy link
Contributor

That sounnds like a bug. Please create an issue on the aurelia-polyfills repo and attach a zip containing the source for a simple reproduction on top of the nav skeleton.

@heruan
Copy link

heruan commented Mar 21, 2016

Opened aurelia/polyfills#17. No zip needed I think, since the bug triggers just adding @singleton to e.g. users.js in the nav skeleton! I'll track the issue there. Thank you!

@Vaccano
Copy link

Vaccano commented May 27, 2016

I just want to say +1 on this feature request. I think this should be considered a "core" feature that should have been done a long while ago.

I have been trying to make it myself and it is hard for someone not familiar with the internals of Aurelia.

@rborosak
Copy link

@Vaccano Just follow what @EisenbergEffect suggested. I'm a total JS noob and I got it working so I believe anyone can.
I have created my own RouteLoader implementation where I read my view model from cache (if it exists) and pass it down the pipeline.
I have also created my ViewFactory that loads my view from cache.
If you need help I'll provide a code sample.

@Vaccano
Copy link

Vaccano commented May 27, 2016

@rborosak I would LOVE a code sample. I am am not doing too well on this one.

@rborosak
Copy link

rborosak commented May 27, 2016

Here is a link. That works for me, but as I said I'm a JS noob so it might not be the best solution :)
Also, I like to add @transient() to my viewmodel classes :)

https://gist.github.com/rborosak/dc505e32a672e60c061dccd048767a98

@Vaccano
Copy link

Vaccano commented May 27, 2016

@rborosak Thank You! I will give it a look.

@Vaccano
Copy link

Vaccano commented May 27, 2016

@rborosak That was really good stuff! Thanks!

Would you be willing to show where/how you plugged into the ViewFactory?

@rborosak
Copy link

rborosak commented May 27, 2016

Here it is, but I'm not sure if it works. I think there is some problem with showing invalid data (red square around invalid text boxes) with cached view so I'm not using view caching. I always create a new. Although I wasn't using the new version of aurelia validation.

https://gist.github.com/rborosak/265ece1aee0628219af0566fb552d721

@jesuslpm
Copy link

jesuslpm commented Jun 24, 2016

I'm also interested in this feature. In the mean time I'm developing hybrid mobile apps with ionic, but I plan to start with Aurelia as I think it's the best javascript framework . Ionic implements this feature out the box. From ionic docs:

By default, views are cached to improve performance. When a view is navigated away from, its element is left in the DOM, and its scope is disconnected from the $watch cycle. When navigating to a view that is already cached, its scope is then reconnected, and the existing element that was left in the DOM becomes the active view. This also allows for the scroll position of previous views to be maintained.

@Scapal
Copy link

Scapal commented Jun 24, 2016

@jesuslpm
Copy link

@Scapal Great!! Thanks!!

@jdanyow jdanyow mentioned this issue Oct 17, 2016
@Vaccano
Copy link

Vaccano commented Jan 25, 2017

@EisenbergEffect - You mentioned this was coming with Aurelia Interface. But now that project is "dead".

Is this feature dead too?

I ask because getting something working here is not too hard, but getting it working well with the router is hard.

And really is something I am supprised is not just "baked in".

@EisenbergEffect
Copy link
Contributor

There's already work being done for this. There are two PRs being worked on by @jdanyow for this.

@alu
Copy link

alu commented Feb 24, 2017

@EisenbergEffect This new feature is in progress? #425 looks like in suspended.

@EisenbergEffect
Copy link
Contributor

Jeremy just moved across the country and took a new job. So, that has slowed down his contributions while he's been making the transition.

@Vaccano
Copy link

Vaccano commented Mar 8, 2017

@jdanyow @EisenbergEffect I have been working on this in my code for days. It is a very hard problem to solve.

The problem is that if you just cache the view that is the routed view you get issues with custom elements that you have composed on to the "parent" view. I have found a way to iterate down the tree of custom elements and custom attributes.

But there is a MAJOR hang up. Items that use Aurelia's repeater don't show like normal composed custom elements. When I get to the repeat I just get a controller that is a "Repeat" controller, and it does not have a view that would let me know what controllers it has in it.

Since this feature is on hold for now, could someone give me a hint on how to get to the "views" in a Repeat controller?

In case it is useful, here is my overridden router view (CachedRouterView) that is trying to dig into the sub elements. Most of the relevant stuff starts with the call to "disableStartupMethodsForControllerChildern".

@EisenbergEffect
Copy link
Contributor

@Vaccano You can get the views from the viewModel on the controller. I do think @jdanyow is going to pick this back up soon though and try to finish it out. Hopefully the two of you can talk shop a bit so he can understand the challenges you are facing.

@Vaccano
Copy link

Vaccano commented May 10, 2017

@EisenbergEffect @jdanyow - I am about to start a significant overhaul on my application to work around this issue. I have spend significant amounts of time trying to "roll my own" on this and, while it may seem easy on the surface, significant knowledge of the inner working of Aurelia is needed to even get close.

I am planning to split all my View Models (just under 20) and store all of the things that should be remembered between tab switches in external classes. My hope is that I can get the correct instance of these classes each time a new view model is made by Aurelia. The Aurelia view models will end up just performing Aurelia events/logic updates to the "real" view models.

I don't want to do this (I am sure it will come with its own pack of bugs), but I keep having too many bugs that come down to this functionality/issue.

Before I make this change, I just want to make sure that there is not hidden development that has been going on for this issue (where I could expect an update soon). (I am guessing not, since this has been open for 1 3/4 years.)

@jdanyow
Copy link
Contributor

jdanyow commented May 21, 2017

I haven't had time to get back to this yet. I'd say your plan is a good approach either way.

@stefan505
Copy link

Does anyone know what the status of this as a built-in feature happens to be?

@CedusJenkins
Copy link

@EisenbergEffect @jdanyow - please can we get an update/eta/confirmation this is dead?

@EisenbergEffect
Copy link
Contributor

Originally, this was funded by a company specifically, but they decided to focus their funding in other areas. As a result, at the time, the work on this stopped. The core team is not actively investing in this above other areas of the router right now. We're always open to community contributions. As an alternative, you may want to consider using a portal to cache heavy screen components outside of the core view for certain routes. @bigopon has written a portal plugin for Aurelia and may have some thoughts on this.

@EisenbergEffect
Copy link
Contributor

Note that the router can treat VMs as singletons to cache those. You can also implement getViewStrategy on your VM instance along with creating a custom ViewStrategy/ViewFactory to enable caching of the view. That's been possible since the beginning, more or less. You could create a ViewStrategy that wraps one of the built-in strategies and just returns a factory that wraps the default factory, but that caches the view instance and always returns the same instance after creation. I've never tried it but it should work, in theory.

@jfstephe
Copy link

Thanks for the detailed and upfront guidance. 😊

@Vaccano
Copy link

Vaccano commented Apr 14, 2018

@jfstephe - just a word of warning. I tried to implement this. It looks as if is should work, but there are problems that cannot be resolved once you get into the details.

See my post above for how I worked around this. What I did was a lot more work, but it functioned perfectly.

@jwx
Copy link
Member

jwx commented Apr 14, 2018

@CedusJenkins @jfstephe Would #538 work for your use cases?

@jfstephe
Copy link

@jwx - It sounds like it thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.