[Tutor] Correct use of model-view-controller design pattern

Peter Otten __peter__ at web.de
Sun May 29 10:28:35 EDT 2016


boB Stepp wrote:

[snip]

No direct answers to your questions, sorry;)

In my experience MVC is nearly always used to build GUIs and based on 
classes. The idea is that you can add multiple views to a model, a bar chart 
and a table of the values, sometimes at runtime. You may also swap out the 
model underneath the view, retrieve text from a file system or a database 
via the same interface. This is hard with modules.

The separation between view controller is often artificial or at least I 
always end up with a mix of these two components. The model-view split on 
the other side (also known as observer pattern) is straight-forward and 
helpful to produce cleaner code; but even between model and view the 
coupling tends to get stronger than you might wish.

Here's a commandline example with two views (the circle-drawing routine has 
to be fixed); in real life I would probably manipulate the model directly 
and omit the controller classes. There are two explict view classes, but the 

while True: ...

loop could also be seen as a view -- or rather the dreaded view/controller 
mix.

$ cat mvc_cli.py
#!/usr/bin/env python3


class CircleModel:
    def __init__(self, radius):
        self._radius = radius
        self.views = []

    def add(self, view):
        self.views.append(view)

    def changed(self):
        for view in self.views:
            view.changed(self)

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        if self._radius != radius:
            self._radius = radius
            self.changed()


class CircleView:
    def __init__(self, model):
        self._model = None
        self.model = model
        self.changed(model)

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, model):
        if self._model is not None:
            raise NotImplementedError
        model.add(self)
        self._model = model

    def changed(self, model):
        """React to model changes."""


class RadiusDeltaController:
    def __init__(self, model, delta):
        self.model = model
        self.delta = delta

    def grow(self):
        self.model.radius += self.delta

    def shrink(self):
        self.model.radius -= self.delta


class RadiusController:
    def __init__(self, model):
        self.model = model

    def set_value(self, value):
        self.model.radius = value


def dist(ax, ay, bx, by):
    return ((ax-bx)**2 + (ay-by)**2)**.5


class CircleImageView(CircleView):

    def changed(self, model):
        self.update_radius(model.radius)

    def update_radius(self, radius):
        # FIXME
        diameter = 2*radius
        cx = cy = radius
        for row in range(diameter):
            for column in range(2*diameter):
                if dist(cx, cy, row, column/2) < radius:
                    print("*", end="")
                else:
                    print(" ", end="")
            print()


class CircleValueView(CircleView):
    def changed(self, model):
        print("Circle radius is now", model.radius)


if __name__ == "__main__":
    circle = CircleModel(5)
    image_view = CircleImageView(circle)
    value_view = CircleValueView(circle)
    rc = RadiusController(circle)
    dc = RadiusDeltaController(circle, 2)
    print("Model-View-Controler Demo")
    print("Enter an integer to set the circle radius")
    print("or 'grow' to increase the radius")
    print("or 'shrink' to decrease the radius")
    print("or 'add' to add another CircleValueView")
    while True:
        try:
            s = input("radius or 'grow' or 'shrink' or 'add': ")
        except KeyboardInterrupt:
            print("\nThat's all folks")
            break
        if s == "grow":
            dc.grow()
        elif s == "shrink":
            dc.shrink()
        elif s == "add":
            CircleValueView(circle)
        else:
            try:
                radius = int(s)
            except ValueError:
                print("don't know what to do with input {!r}".format(s))
            else:
                rc.set_value(radius)


Here's a sample session:
$ python3 mvc_cli.py 
                    
     ***********    
   ***************  
 *******************
 *******************
 *******************
 *******************
 *******************
   ***************  
     ***********    
Circle radius is now 5
Model-View-Controler Demo
Enter an integer to set the circle radius
or 'grow' to increase the radius
or 'shrink' to decrease the radius
or 'add' to add another CircleValueView
radius or 'grow' or 'shrink' or 'add': add
Circle radius is now 5
radius or 'grow' or 'shrink' or 'add': grow
                            
       ***************      
     *******************    
   ***********************  
  ************************* 
 ***************************
 ***************************
 ***************************
 ***************************
 ***************************
  ************************* 
   ***********************  
     *******************    
       ***************      
Circle radius is now 7
Circle radius is now 7
radius or 'grow' or 'shrink' or 'add': grow
                                    
          *****************         
       ***********************      
     ***************************    
    *****************************   
  ********************************* 
  ********************************* 
 ***********************************
 ***********************************
 ***********************************
 ***********************************
 ***********************************
  ********************************* 
  ********************************* 
    *****************************   
     ***************************    
       ***********************      
          *****************         
Circle radius is now 9
Circle radius is now 9
radius or 'grow' or 'shrink' or 'add': 6
                        
      *************     
    *****************   
  ********************* 
 ***********************
 ***********************
 ***********************
 ***********************
 ***********************
  ********************* 
    *****************   
      *************     
Circle radius is now 6
Circle radius is now 6
radius or 'grow' or 'shrink' or 'add': add
Circle radius is now 6
radius or 'grow' or 'shrink' or 'add': ^C
That's all folks
$

There's also a tkinter version (which I wrote before the cli one). I think 
it's easier to read the complete code than the diff; so there, with some 
repetition:

$ cat mvc.py
#!/usr/bin/env python3
import tkinter as tk


class CircleModel:
    def __init__(self, radius):
        self._radius = radius
        self.views = []

    def add(self, view):
        self.views.append(view)

    def changed(self):
        for view in self.views:
            view.changed(self)

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        if self._radius != radius:
            self._radius = radius
            self.changed()


class CircleView:
    def __init__(self, model):
        self._model = None
        self.model = model
        self.changed(model)

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, model):
        if self._model is not None:
            raise NotImplementedError
        model.add(self)
        self._model = model

    def changed(self, model):
        """React to model changes."""


class RadiusDeltaController:
    def __init__(self, model, delta):
        self.model = model
        self.delta = delta

    def grow(self):
        self.model.radius += self.delta

    def shrink(self):
        self.model.radius -= self.delta


class RadiusController:
    def __init__(self, model):
        self.model = model

    def set_value(self, value):
        self.model.radius = value


class CircleImageView(CircleView):
    def __init__(self, root, model, fill, **kw):
        self.canvas = tk.Canvas(root, width=100, height=100)
        self.fill = fill
        self.canvas.grid(**kw)
        self.id = None
        super().__init__(model)

    def changed(self, model):
        self.update_radius(model.radius)

    def update_radius(self, radius):
        id = self.id
        cx = cy = 50
        extent = cx-radius, cy-radius, cx+radius, cx+radius
        if id is None:
            self.id = self.canvas.create_oval(
                *extent, fill=self.fill)
        else:
            self.canvas.coords(id, *extent)


class ResizeView(CircleView):
    def __init__(self, root, model, controller, **kw):
        self.frame = tk.Frame(root)
        self.frame.grid()
        self.grow = tk.Button(
            self.frame, text="Grow", command=controller.grow)
        self.grow.pack(side=tk.LEFT)
        self.shrink = tk.Button(
            self.frame, text="Shrink", command=controller.shrink)
        self.shrink.pack(side=tk.LEFT)
        super().__init__(model)

    def changed(self, model):
        self.grow["text"] = "Grow to {}".format(model.radius + 10)
        self.shrink["text"] = "Shrink to {}".format(model.radius - 10)


class RadiusView(CircleView):
    def __init__(self, root, model, controller, **kw):
        self.controller = controller
        self.frame = tk.Frame(root)
        self.frame.grid(**kw)
        self.label = tk.Label(self.frame, text="Radius")
        self.label.grid(row=0, column=0)
        self.hint = tk.Label(self.frame, text="Hit <Return> to apply")
        self.hint.grid(row=0, column=3)
        self.textvariable = tk.StringVar()
        self.entry = tk.Entry(self.frame, textvariable=self.textvariable)
        self.entry.bind("<Key-Return>", self.set_value)
        self.entry.grid(row=0, column=1)
        super().__init__(model)

    def set_value(self, *args):
        try:
            radius = int(self.textvariable.get())
        except ValueError:
            pass
        else:
            self.controller.set_value(radius)

    def get_value(self, model):
        return str(model.radius)

    def changed(self, model):
        self.textvariable.set(self.get_value(model))


if __name__ == "__main__":
    root = tk.Tk()
    circle = CircleModel(42)
    red_view = CircleImageView(
        root, circle, "red",
        row=0, column=0)
    green_view = CircleImageView(
        root, circle, "green",
        row=0, column=1)
    resize_view = ResizeView(
        root, circle, RadiusDeltaController(circle, delta=10),
        row=1, column=0, columnspan=2)
    radius_view = RadiusView(
        root, circle, RadiusController(circle),
        row=2, column=0, columnspan=2)

    root.mainloop()
$



More information about the Tutor mailing list