Navigating between tabbed pages does not refresh page

I have a multi-page app using the Bootstrap template. Two of these pages have tabs. Initial navigation works but subsequent switching between tabs does not refresh the content in the main area. Navigating to a non-tabbed page works fine. No error messages appear, so I have no idea how to fix this. Any help is appreciated.

Panel 1.2.3, python 3.9.16, browser is Chrome on Linux.

Minimum working example below:

import panel as pn
pn.extension()
###################################################################
class P1:
    def __init__(self):
        self.T1C = pn.pane.Markdown("## Content for Page 1, Tab1",name='Page 1, Tab1')
        self.T2C = pn.pane.Markdown("## Content for Page 1, Tab2",name='Page 1, Tab2')
   
    def GetLayout(self):
        self.Tabs = pn.Tabs(dynamic=True)
        self.Tabs.extend([self.T1C,self.T2C])
        self.layout = pn.Column('Hello World from Page 1',self.Tabs)
        return (self.layout)
###################################################################    
class P2:
    def __init__(self):
        self.T1C = pn.pane.Markdown("## Content for Page 2, Tab1",name='Page 2, Tab1')
        self.T2C = pn.pane.Markdown("## Content for Page 2, Tab2",name='Page 2, Tab2')
        self.T3C = pn.pane.Markdown("## Content for Page 2, Tab3",name='Page 2, Tab3')
   
    def GetLayout(self):
        self.Tabs = pn.Tabs(dynamic=True)
        self.Tabs.extend([self.T1C,self.T2C,self.T3C])
        self.layout = pn.Column('Hello World from Page 2',self.Tabs)
        return (self.layout)
###################################################################
pages = {
    "Welcome": pn.Column('Welcome Page'),
    "P1": P1().GetLayout(),
    "P2": P2().GetLayout(),
}
NavGrouping = {'Start':['Welcome'],'Pages':['P1','P2']}
###################################################################
def show(page):
    return pages[page]

def appmain():
    starting_page = pn.state.session_args.get("page", [b"Welcome"])[0].decode()
    PageSelector = pn.widgets.Select(
        value=starting_page,
        #options=['P1','P2'],
        groups=NavGrouping,
        name="Navigation",
        width=200
        )
    ishow = pn.bind(show, page=PageSelector)
    pn.state.location.sync(PageSelector,{"value": "page"})  # Shows up in URL as ?page=<SelectedPage>

    TLayout = pn.template.BootstrapTemplate(
        title="MWE Test",
        sidebar=[PageSelector],
        main=[ishow]
    ).servable()
    return (TLayout)
###################################################################
appmain()

I ran your example and it seems to work just fine. I could switch from the main to the tabbed pages, open tabs, and go back. Maybe you can elaborate on what the problem is.

Thanks very much for trying this out. The sequence is:

  1. Navigate to P1
  2. Navigate to P2
  3. Navigate back to P1.

Page stays as P2. Using a browser refresh does bring back page P1 in the main area.

I am wondering if this is something to do with browser caching.

Thanks.

Using some prints shows the panel app seems to be modifying the pages dict and overwriting the keys. Maybe @Marc understands better what is happening here.

It looks like Panel is modifying your columns in place by swapping out the inner contents, mutating the columns you’ve stored in the pages dict. I changed the following to make fresh objects each time you change page:

class P1:
    def GetLayout(self):
        T1C = pn.pane.Markdown("## Content for Page 1, Tab1",name='Page 1, Tab1')
        T2C = pn.pane.Markdown("## Content for Page 1, Tab2",name='Page 1, Tab2')
        Tabs = pn.Tabs(dynamic=False)
        Tabs.extend([T1C,T2C])
        layout = pn.Column('Hello World from Page 1',Tabs)
        return (layout)
###################################################################    
class P2:
    def GetLayout(self):
        T1C = pn.pane.Markdown("## Content for Page 2, Tab1",name='Page 2, Tab1')
        T2C = pn.pane.Markdown("## Content for Page 2, Tab2",name='Page 2, Tab2')
        T3C = pn.pane.Markdown("## Content for Page 2, Tab3",name='Page 2, Tab3')
        Tabs = pn.Tabs(dynamic=False)
        Tabs.extend([T1C,T2C,T3C])
        layout = pn.Column('Hello World from Page 2',Tabs)
        return (layout)
###################################################################
pages = {
    "Welcome": pn.Column('Welcome Page'),
    "P1": P1(),
    "P2": P2(),
}
NavGrouping = {'Start':['Welcome'],'Pages':['P1','P2']}
###################################################################
def show(page):
    r = pages[page]
    return r.GetLayout() if hasattr(r, 'GetLayout') else r

… everything seems to work fine, but try to select any tab other than the first, then change pages and you get this:

ValueError: Tab names do not match objects, ensure that the Tabs.objects are not modified directly. Found 3 names, expected 2.
Exception in callback functools.partial(<bound method IOLoop._discard_future_result of <tornado.platform.asyncio.AsyncIOMainLoop object at 0x000001FD93CBB430>>, <Task finished name='Task-17460' coro=<ServerSession.with_document_locked() done, defined at \bokeh\server\session.py:77> exception=ValueError('Tab names do not match objects, ensure that the Tabs.objects are not modified directly. Found 3 names, expected 2.')>)
Traceback (most recent call last):
  File "tornado\ioloop.py", line 740, in _run_callback
    ret = callback()
  File "tornado\ioloop.py", line 764, in _discard_future_result
    future.result()
  File "bokeh\server\session.py", line 98, in _needs_document_lock_wrapper
    result = await result
  File "panel\reactive.py", line 428, in _change_coroutine
    state._handle_exception(e)
  File "panel\io\state.py", line 436, in _handle_exception
    raise exception
  File "panel\reactive.py", line 426, in _change_coroutine
    self._change_event(doc)
  File "panel\reactive.py", line 444, in _change_event
    self._process_events(events)
  File "panel\reactive.py", line 383, in _process_events
    self.param.update(**self_events)
  File "param\parameterized.py", line 1898, in update
    self_._batch_call_watchers()
  File "param\parameterized.py", line 2059, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "param\parameterized.py", line 2021, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "panel\param.py", line 869, in _replace_pane
    self._update_inner(new_object)
  File "panel\pane\base.py", line 704, in _update_inner
    new_pane, internal = self._update_from_object(
  File "panel\pane\base.py", line 678, in _update_from_object
    cls._recursive_update(old, new)
  File "panel\pane\base.py", line 649, in _recursive_update
    old.param.update(**new_params)
  File "param\parameterized.py", line 1898, in update
    self_._batch_call_watchers()
  File "param\parameterized.py", line 2059, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "param\parameterized.py", line 2021, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "panel\reactive.py", line 373, in _param_change
    self._apply_update(named_events, properties, model, ref)
  File "panel\reactive.py", line 301, in _apply_update
    self._update_model(events, msg, root, model, doc, comm)
  File "panel\layout\base.py", line 95, in _update_model
    children, old_children = self._get_objects(model, old, doc, root, comm)
  File "panel\layout\tabs.py", line 165, in _get_objects
    raise ValueError('Tab names do not match objects, ensure '
ValueError: Tab names do not match objects, ensure that the Tabs.objects are not modified directly. Found 3 names, expected 2.

@gib,

Thanks for trying out the code. I have been getting the errors you report in my app, so I tried to create a minimum working example. But my MWE had an issue with the navigation without raising the errors. Either way, navigation and tabbed pages are not playing nice.

This type of problem began when I upgraded Panel. I have verified that the code works fine with Panel 0.14.4.

This seems to fix it for me. I am not aware of anything in the docs that explains the mutation behavior, although this has bitten me a few times. Maybe @philippjfr can explain what is happening?

import copy
def show(page):
  out = pages[page]
  return copy.deepcopy(out)

I think what’s going on here is an optimization behind the scenes whenever you replace a layout component (eg. Column, or Tab, etc.) with another one of the same type Panel is just swapping in the new inner components. Tabs does not seem to work properly when this happens, it appears like the tab groups are not copied in correctly.

It looks like the way around it right now is to only store a single Tab component and have your callbacks directly update the inner structure.

Does Column().clone work?

I verified that this fixes the problem reported above.

I have noticed that the problem occurs for versions of Panel after 0.14.4. Release notes say version 1.0.0 made the transition from Bokeh 2.x to 3.x. Perhaps that caused the issue?

Thanks for your help.

Copying/Cloning still doesn’t resolve the issue if you try to switch pages when you have selected any tab other than the first tab, example:

Navigate to P2
select Page 2, Tab 3
navigate to P1
ValueError exception and P1 tabs are wrong

Should this be filed as a bug? To me this type of behavior should either be fixed or documented