'How to implement a tkinter app with an MVC architecture?

I currently started with python and the MVC architecture. I use MVC because I've read that its very easy to display simple GUI applications with this architecture.

I've read about the MVC architecture, but when trying to implement it myself I have several issues. I think I didn't quite understand the connection between the model, view and controller.

With my program I want to display a button. When the button is pressed a file selection opens and the user can select a file. Afterwards the filename should be written in the entry box.

The problem I'm facing is, that I dont know how to connect the view with the controller and model. So when the button is pressed I want to inform my controller about it, so he manage the file selection. After the file selection I want to save the path in the model.

Here is my code:

View

from tkinter import filedialog
import tkinter as tk

class View(object):
    def __init__(self, controller):
       self.controller = controller

    def showGUI(self, title):
         self.projectWindow = tk.Tk()
         self.projectWindow.title(title)

         self.importEntry= tk.Entry(projectWindow, width=100)
         self.importEntry.pack(pady=10,padx=10)

         importButton = tk.Button(projectWindow, text ="Import File", command= lambda : self.controller.importButtonPressed(controller)) #Here I need the connection to the controller but I cant access the controllers methods
          #I know this is a very bad practise but I dont really know how to inform the controller in a other way 

         importButton.pack(pady=10, padx=10,side=tk.LEFT)

    def display_file_selection_view(self):
       file_path = filedialog.askopenfilename(title = "Select a Excel File", filetypes=[("Excel files", ".xlsx .xls")])
       return file_path

Controller

import Model 
import View
class Controller(object):
   def __init__(self, model, view):
       self.model = model
       self.view = view

       #I tried something like this to inform the model about the state change
       self.model.register_observer(self.view.importEntry)


  def update(self):
    self.view.importEntry.config(state="normal")
    self.view.importEntry.delete(0,'end')
    self.view.importEntry.insert(0,self.model.selectedElement)
    self.view.importEntry.config(state="readonly")

  def importButtonPressed(self):
    selectedElement = self.view.display_file_selection_view()

    #check if data has been selected
    if selectedElement:
        self.model.selectedExcel = selectedElement
    else:
        self.view.showGUI("File Selection")

Model

 class Model(object):
     def __init__(self):
        self._args = ""
        self._selectedExcel = ""
        self.observers = []
    
    def register_observer(self, observer):
        self.observers.append(observer)
    
    def notify(self):
        [observer.update() for observer in self.observers]

    @property
    def selectedExcel(self):
        return self._selectedExcel

    @selectedExcel.setter
    def selectedExcel(self, value):
       self._selectedExcel = value
       self.notify()

Main

  import Model
  import View 
  import Controller 
  if __name__ == "__main__":
     model = Model.Model()
     view = View.View(Controller)
     controller = Controller.Controller(model, view)


Solution 1:[1]

Your controller class object needs to be initialized in the View class.

def __init__(self,controller):
    self. controller = controller

The flaw in this bit of code is that controller is always going to be an empty object. And you are basically assigning the value of an empty object to your instance variable.

What you need to is instantiate an instance of the controller class in your View constructor like so

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

You need to correct the variables model and view to objects of model and view constructors.

Solution 2:[2]

The point of MVC is encapsulating elements. It means dependencies always goes in one direction. View cannot "know" the Controller. The Controller have to "know" the View. The way to keep M V and C separate is to make them sockets and plugs.

View

from tkinter import filedialog
import tkinter as tk

class View(object):

    # The view will only be responsible for displaying widgets
    # and getting some information from user

    def __init__(self):
        # it means it arranges all widgets just as it initialize
        self.callbacks = {}
        self._showGUI("File Selection")

    def _showGUI(self, title):
        # This method will only build widgets. I prefixed it underline,
        # as it is private method of the view class, and shouldn't be
        # used outside of it (i.e. by the controller)
        self.projectWindow = tk.Tk()
        self.projectWindow.title(title)

        self.importEntry= tk.Entry(self.projectWindow, width=100)
        self.importEntry.pack(pady=10,padx=10)

        self.importButton = tk.Button(self.projectWindow, text ="Import File")
        # In this moment we don't define command of this button
        self.importButton.pack(pady=10, padx=10,side=tk.LEFT)

    def add_callback(self, key, method):
        # This method makes connections, and lets the
        # controller share its own methods to view, and prevents
        # view being dependent on controller. This method will be
        # used specifically by the controller.

        self.callbacks[key] = method

    def bind_commands(self):
        # This method also will be used by the controller. And this
        # going to happened at the moment, when callbacks will be defined

        self.importButton.config(command=self.callbacks['import'])

    def run(self):
        # This method lets program works. It also is used by the controller
        self.projectWindow.mainloop()

    def get_file_selection_view(self):
        file_path = filedialog.askopenfilename(title = "Select a Excel File", filetypes=[("Excel files", ".xlsx .xls")])
        return file_path

    def display_selection(self, content):
        self.importEntry.config(state="normal")
        self.importEntry.delete(0,'end')
        self.importEntry.insert(0,self.model.selectedElement)
        self.importEntry.config(state="readonly")

Controller

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

        # Here we pass to the view controller method without making view
        # dependent on it
        self.view.add_callback('import', self.importButtonPressed)

        # Now, callback exist, so the controller can ask the view to bind
        # its widgets to passed values of callbacks dictionary
        self.view.bind_commands()

        self.view.run()

    def importButtonPressed(self):
        # This method, as its controller method, should lead all actions

        # First it gets file path
        selectedElement = self.view.get_file_selection_view()

        # Check if data has been selected
        if selectedElement:
            # Here it should use some model method to do whatever you expect
            # to model do, and return result
            # Honestly - I don't know what do you want from your model
            # But whatever it is let model do it itself, and return result here.
            result = self.model.some_method(selectedElement)
        else:
            pass

        # Now you can easily display result. But still, to stick with MVC
        # architecture you should use view method to got it done, instead of
        # directly using some view objects method. It should work (both view and 
        # model), like you're
        # going to make in the future another View class with same interface
        # (all public methods and parameters) and all you need to do to start using 
        # your new class, would be change imports in main.py

        self.view.display_filename(result)

Main

import model
import view
import controller


if __name__ == "__main__":
    model = model.Model()
    # The view doesn't need to have a controller parameter
    view = view.View()
    controller = controller.Controller(model, view)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 kubablo