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.