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