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)
我正在考虑如何为图形布局代码 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)