MVC 架构中的文本输入

Text entry in MVC architecture

我正在考虑如何为图形布局代码 UI。我使用的是 Python 和 WX:这并不是这里问题的重点,但让我们以它为例。

我理解 MVC 的意义,这是我之前尝试实现的东西,但我似乎经常陷入纠结,接口泄漏,一切看起来有点不愉快,如果有点工作。这一次,我想做对了(TM)。

我(认为我)通常遇到的问题是 UI 控件的固有状态,例如文本输入。在 WX wiki's MVC example 中,MVC 由一个 "Money" 模型和一个单独的控制器小部件中的 "add" 和 "remove" 动作组成,并使用 非可编辑 文本条目以在视图中显示它。

但是,我想要实现的是一个 UI,它有一个 可编辑的 字符串文本条目(假设它是一个 "name") .用户可以在框中键入内容,然后某些后端会生成建议列表。这些将显示在 UI 中的列表中。如果用户选择其中之一,条目将更新为该值,或者用户可以继续输入以获得更精确的建议。确认(例如 "enter" 或某个按钮)将对输入框的当前值执行一些操作。这与许多网络浏览器的自动完成地址栏基本相同。

然而,这似乎暗示输入文本框是视图 和控制器 的一部分,因为它都显示当前 "name"(无论是键入或从后端建议中选择)并提供来自用户的事件(在本例中为文本更改事件)。如果不出意外,天真的实现将以无限递归结束,因为对条目的修改将导致更新,这将修改条目...

这是一个缩减示例,没有建议后端。输入框位于 View 中,Controller 可以伸手从 View 中获取值 if (self.get_name() != new_name): 避免递归,在这种情况下,由于文本首先来自此框,因此控件永远不会在视图中更新。

import wx

from wx.lib.pubsub import pub

class NameModel(object):
    def __init__(self):
        self.name = ''

    def set_name(self, new_name):
        self.name = new_name

        print("Model name is now: %s" % new_name)

        # name has changed - tell anyone who cares
        # (maybe bailiffs?)
        pub.sendMessage("NAME CHANGED", name=self.name)

class NameView(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title="Name View")

        sizer = wx.BoxSizer(wx.VERTICAL)
        self.name_ctrl = wx.TextCtrl(self)

        sizer.Add(self.name_ctrl, 0, wx.EXPAND | wx.ALL)

        self.name_ctrl.SetEditable(True)
        self.SetSizer(sizer)

    def get_name(self):
        '''
        Why does the View have a getter?
        '''
        return self.name_ctrl.GetValue()

    def set_name(self, new_name):

        # check for infinite recursion, but this may need to be aware of
        # more state so we don't throw away useful updates
        # Or perhaps disconnect the event?
        if (self.get_name() != new_name):
            print("Setting name in view: %s" % new_name)
            self.name_ctrl.SetValue(new_name)


class NameController(object):

    def __init__(self, app):

        self.model = NameModel()

        self.view = NameView(None)

        # bind events to wire everything up

        # hmm, this feels a bit wierd - the text entry is in the View,
        # but it's driving the Controller here?
        self.view.name_ctrl.Bind(wx.EVT_TEXT, self.update_name)

        # subscribe the the model messages
        pub.subscribe(self.name_changed, "NAME CHANGED")

        self.view.Show()

    def update_name(self, evt):
        '''
        Called when the user has set the name somehow
        '''
        # ??? getting the data _from_ the view ???
        new_name = self.view.get_name()
        self.model.set_name(new_name)

    def name_changed(self, name):
        '''
        The model has changed, update the view(s) as needed
        '''

        self.view.set_name(name)


if __name__ == "__main__":

    app = wx.App(False)
    controller = NameController(app)
    app.MainLoop()

这是将有状态 UI 小部件集成到 MVC 样式程序中的正确方法吗?感觉有点不整洁,因为小部件同时是 "View" 的一部分和 "Controller" 的一部分。正因为如此,我担心 "should update" 的检查在代码维护方面会变得繁重,而且问题不会完全分离,从而导致复杂性。

为什么 View 有一个 getter?您需要知道用户在文本框中写了什么。您可以使用 getter 或传递给绑定到事件的回调的参数(在本例中为 wx.EVT_TEXT)。如果工具包没有提供你想要的方法(免责声明:我忽略了关于wxwidgets的一切),你可能需要在视图中实现它或者写一个适配器。

检查无限递归:将检查放在模型中。国家属于它,所以它不会觉得那里不整洁。另外在模型中设置名称并不一定意味着名称更改,仅当名称实际更改时才发送 "NAME CHANGED" 对我来说有意义并且不会感到不整洁或繁琐。

def set_name(self, new_name):
    old_name = self.name
    self.name = new_name

    print("Model name is now: %s" % new_name)
    if old_name != new_name:
        # name has changed - tell anyone who cares
        # (maybe bailiffs?)
        pub.sendMessage("NAME CHANGED", name=self.name)

在控制器中绑定视图事件:这在我看来是可以接受的。在您的代码中,控制器是唯一知道视图的控制器,因此它是唯一可以进行绑定的控制器。当然还有其他方法,但不知道你的书是怎么说的,我无法提供具体的答案。

视图(在控制器中)获取数据:正如我上面所说,update_name() 可以接收数据作为参数,也许作为evt.

而不是使用

self.name_ctrl.SetValue(new_name)

这会触发一个 wxEVT_TEXT 事件

使用

self.name_ctrl.ChangeValue(new_name)

不会触发 wxEVT_TEXT 事件

如果需要保持插入点使用

insertion_point = self.name_ctrl.GetInsertionPoint()
self.name_ctrl.ChangeValue(new_name)
self.name_ctrl.SetInsertionPoint(insertion_point)