Passing button callbacks from view to controller (MVC)

Summary

I would like to link my button on_click function to a function in the controller class.
Whilst keeping the controller class Streamlit free.

Full story

I’m working on a budgeting dashboard. The prototype is done but I would like to refactor everything using the MVC pattern. This would result in cleaner code, faster implementation etc…

I found a couple of sources that tried the same thing but was not completely satisfied with their implementation.

Ideally, I would want something similar to how QT makes it possible to connect a button with a function inside the controller

class View(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.layout = QVBoxLayout()
        self.button = QPushButton("Click me!")
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

class Controller:
    def __init__(self, model, view):
        self.model = model
        self.view = view
        self.view.button.clicked.connect(self.handle_button_click)

    def handle_button_click(self):
        print(self.model.data)

But this does not seem to be possible as you are forced to define the on_click function within the declaration of the button itself. Something like this would be ideal:

class View:
    def __init__(self):
        self.button_a = st.button("Click me")
    
    def display_message(self, message):
        st.write(message)


class Controller:
    def __init__(self, model, view):
        self.model = model
        self.view = view
        self.setup_callbacks()
    
    def setup_callbacks(self):
        self.view.button_a.on_click(self.handle_button_click)
    
    def handle_button_click(self):
        self.view.display_message("Button has been clicked!")

My current implementation ( that I’m not that happy with) uses the state system and an if statement in my controller (:unamused:) to catch the update.
This code still has issues updating the UI with the return value of the controller Class function.

Steps to reproduce

If u press the “Run” button It prints the message from the “display_message” function but it does not update the “session_state.result” string until I press it again. This means that I have to press it twice to update the result string.

Code snippet:

import streamlit as st


class Model:
    def __init__(self):
        self.data = "pile of data"


class View:
    def __init__(self):
        if "click" not in st.session_state:
            st.session_state.click = False

        if "result" not in st.session_state:
            st.session_state.result = "Press the button to calculate"

        st.write(st.session_state)
        self.drawUI()
        self.session_state = st.session_state

    def drawUI(self):
        runButton = st.button(
            "Run", on_click=self.onButtonClickFunction, kwargs={"key": "click"}
        )
        st.text(st.session_state.result)

    def onButtonClickFunction(self, key):
        st.session_state[key] = True

    def display_message(self, message):
        st.write(message)

    def set_result(self, value):
        st.write("result: {}".format(value))
        st.session_state.result = value


class Controller:
    def __init__(self, model, view):
        self.model = model
        self.view = view

        if self.view.session_state.click:
            self.view.display_message("message from controller")
            # st.session_state.result = "123"
            # self.view.session_state.result = "456"
            self.view.set_result("789")


def main():
    st.title("Button MVC Demo")

    model = Model()
    view = View()
    controller = Controller(model, view)


if __name__ == "__main__":
    main()

Expected behavior:

It would expect to update the session state to be updated and when it redraws the view that the value would be shown.

Actual behavior:

It only updates the session state when the button is pressed again, never on the first press.

Additional information

On line 44 - 47 ( if self.view.session_state.click: … )

you can see the different methods I tried for this problem.
I would like to know what the best course of action would be when implementing such a design pattern. Or am I misunderstanding something about the MVC pattern itself?

Thank you in advance!

Debug info

  • Streamlit version: version 1.21.0
  • Python version: 3.10.6
  • Python venv
  • OS version: WSL2 Ubuntu - Windows 11
  • Browser version: Chrome Version 113.0.5672.127

Requirements file

The snippet above and streamlit module should do it.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.