Callbacks from factory not firing


#1

My app is database backed, so for every record of one model there have to be same interface part generated, with 3 buttons each, 2 of them have to fire callback, but id’s of those buttons are unknown until page layout is generated (then database is queried for all ids).

I have created something factory-like to generate those callback:

    def create_callbacks():
        def delete(i):
            def callback(*data):
                instance = db_model.MyModel.query.get(i)
                instance.delete()
                return 'Removed'
    
            return callback
    
        def toggle(i):
            def callback(*data):
                instance = db_model.MyModel.query.get(i)
                # some actions on instance
                return str(instance)
    
            return callback
        try:
            for model_id in [i for (i,) in db_model.MyModel.query.with_entities(db_model.MyModel.id).all()]:
                try:
                    app.callback(Output('model-{}'.format(model_id), 'children'),
                                 events=[Event('model-{}-delete-button'.format(model_id), 'click')])(delete(model_id))
    
                    app.callback(Output('model-{}-markdown'.format(model_id), 'children'),
                                 events=[Event('model-{}-toggle-button'.format(model_id), 'click')])(toggle(model_id))
                except CantHaveMultipleOutputs:
                    continue
        except OperationalError:
            # log db error

That function gets called inside of, as it is multi-page app:

    @app.callback(
        Output('page', 'children'),
        [Input('location', 'pathname')])
    def display_content(pathname):
        # some ifs
            create_callbacks()
            content = create_content() # create_content() generates copies of one div for every instance putting instance id in button's id
            return content

When I checked app.callbacks_map for their existence, those callbacks were present, with proper components ids. But buttons do nothing on first page load, I have to reload it for buttons to work as intended.

Any ideas what can cause that behavior?


#2

I’m not sure what’s going wrong here. Could you create a small, reproducable example?

At first glance, this part of the code looks wrong to me:

these callback functions should be decorating a function, like

@app.callback(Output('model-{}'.format(model_id), 'children'),
                      events=[Event('model-{}-delete-button'.format(model_id), 'click')])(delete(model_id))
def some_function():
    ...

#3

Defining them with decorator makes defining one function over and over again in loop, just with different argument, that why I provided function as app.callback() argument in app.callback(args, kwargs)(delete(model_id)).

Changing that to decorator does not help anyway. I will try to make some snippet with reproducible example.

@edit:

Here is an example:

    from datetime import datetime

    from dash import dash

    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output, Event, State
    from dash.exceptions import CantHaveMultipleOutputs

    app = dash.Dash(__name__)
    app.config.supress_callback_exceptions = True

    IDS = [1, 2, 3]


    def divs_list():
        return [html.Div([
            dcc.Markdown(
                '',
                id='model-{}-markdown'.format(id)
            ),
            html.P(
                '',
                id='model-{}-p'.format(id)
            ),
            html.Button(
                'Delete',
                id='model-{}-delete-button'.format(id),
                style={'width': '49%'}
            ),
            html.Button(
                'Start/Stop',
                id='model-{}-toggle-button'.format(id),
                style={'marginLeft': '2%', 'width': '49%'}
            ),

            html.Hr()
        ], id='model-k2-{}'.format(id)) for id in IDS]


    def create_callbacks():
        def delete(i):
            def callback(*data):
                return """# Delete
    {}""".format(datetime.now().time().isoformat())

            return callback

        def toggle(i):
            def callback(*data):
                return "Toggle {}".format(datetime.now().time().isoformat())

            return callback

        for model_id in IDS:
            try:
                app.callback(Output('model-{}-markdown'.format(model_id), 'children'),
                             events=[Event('model-{}-delete-button'.format(model_id), 'click')])(delete(model_id))

                app.callback(Output('model-{}-p'.format(model_id), 'children'),
                             events=[Event('model-{}-toggle-button'.format(model_id), 'click')])(toggle(model_id))
            except CantHaveMultipleOutputs:
                continue


    def layout():
        divs = divs_list()

        return [html.Div([
            html.H1('Example'),

            html.Div(divs, id='model-k2-divs'),
        ], style={'maxWidth': '720px', 'margin': '0 auto'}, id='example')]

    app.layout = html.Div(children=[
        html.Meta(
            name='viewport',
            content='width=device-width, initial-scale=1.0'
        ),

        html.Div([
            html.Div([
                html.Div(
                    html.Div(id="page", className="content"),
                    className="content-container"
                ),
            ], className="container-width")
        ], className="background"),

        dcc.Location(id='location', refresh=False),
    ], style={'width': '70%', 'margin': '0 auto'})


    toc = html.Div([
            html.Ul([
                html.Li(dcc.Link('Home', href='/')),
                html.Li(dcc.Link('Example', href='/example')),
            ])
        ])

    pages = {
        'index': {
            'url': '/',
            'content': html.Div([
                html.H1('Table of Content'),
                toc,
            ], className="toc", style={'textAlign': 'center'})
        },
        'example': {
            'url': '/example',
            'content': layout
        }
    }


    @app.callback(
        Output('page', 'children'),
        [Input('location', 'pathname')])
    def display_content(pathname):
        if pathname is None:
            return html.Div()
        matched = [c for c in pages.keys()
                   if pages[c]['url'] == pathname]

        if matched and matched[0] == 'example':
            create_callbacks()
            c = pages[matched[0]]['content']
            page_content = c()
            content = html.Div(page_content)
        else:
            content = pages['index']['content']

        return content

    app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})

    if __name__ == '__main__':
        app.run_server(
            debug=True,
            threaded=True,
            port=5005)

#4

Have you seen my reproducible snippet? I could not find any way to fix that.


#5

Thanks for the example! I see what’s going on here now. Right now, it’s not possible to dynamically create callbacks after the app starts. They need to be created in advance.

This means that it’s impossible to create these types of dynamic interfaces, where new callbacks are defined as new components are rendered.

I’d like to be able to support this in future versions of Dash. If any company out there would like to prioritize this work through a sponsorship, please reach out.


#6

Maybe you can think of a way to make page automatically refresh just after first loading? Buttons work after reload, I have tried to make link to that page html.A instead of dcc.Link as that makes it reload, but that is not enough. It have to fully load and then be reloaded somehow.


#7

Ok. I have figured out a solution.

The link to that page is actually a normal Flask route in html.A:

toc = html.Div([
        html.Ul([
            html.Li(dcc.Link('Home', href='/')),
            html.Li(html.A('Example', href='/load-example')),
        ])
    ])

And that Flask route fires create_callbacks():

@server.route('/load-example'))
def redirect_example():
    create_callbacks()
    return redirect('/example')

That way, after redirect callbacks are already created and Dash handles everything like earlier, but buttons are working as the ‘refresh’ occurred before loading example page.


#8

Hello, I am actually having the same problem right now and I couldn’t quite understand your solution, can you explain it further or post a snippet of the improved code?

Also, has Dash done anything about that problem since August, 2017?

Thanks,


#9

Just pre-create callbacks in advance, even if it means some of them (most of them??) will not be required. In practice, I doubt they will introduce a lot of overhead. While not perfect, I don’t think this is a major limitation in dash, atm.
You will need to set app.config.suppress_callback_exceptions = True for this to work though.