尽管MVC模式很简单,但往往不被理解。首先,您需要了解所有这些分离组件中应该保存什么以及它们如何相互交流。
视图:任何UI元素。一个好的可重用视图元素应该在任何地方都可以重用。因此,视图不知道它的上下文或它将与之交互的具体控制器。视图知道的是它自己的状态和一个(或多个)通用侦听器,一旦发生某些操作,它将调用这些侦听器。视图元素知道它的状态,广播更改,但不应自行更改其状态。例如:按钮将广播一个“点击”操作,并且您将通过控制器通过像
aButton.setLabel("click me!");
这样的方法设置按钮名称。
模型:模型能够处理数据集的状态,通常实现一些函数以将其状态保存和加载到/从文件中。模型是一个盒子,除非有人(控制器)请求,否则不应更改其状态。同样,一个好的模型不知道视图甚至控制器。它就是它的样子。一个非常简单的模型是一个字符串。您可以从文件加载字符串,将其保存到文件,将其更改为大写字母,子字符串等。字符串具有状态,不知道它的上下文,只是等待有人使用它。如果模型知道控制器,您就会被愚弄,除非您实现相同的控制器(即使使用接口也是不好的实践)。
控制器:控制器实际上是您的程序,这是决策部分。它在哪里?那么...您的
main函数
已经是控制器。再次说明,所有决策都在控制器中进行。当有人单击按钮时,将调用控制器中的一个方法(而不是视图中的方法),当您想要更新标签时,您可以从控制器中执行操作。此外,所有线程都应该在控制器中处理。您的模型正在加载大文件吗?在控制器中创建一个线程,要求模型加载图像,然后在完成后通知主线程(超出主题,请搜索事件循环)
多个窗口,我们来了!
视图/控制器如何相互交互?
视图的责任仅是向控制器提供事件(文本字段更改,单击按钮等),并能够以我们想要的方式显示。例如window.setTitle("myWindow");我们可能会尝试将应该在控制器中的东西放入视图中,反之亦然。
例如,如果您正在构建一个警告面板,您将有2个按钮(确定、取消)和一个文本标签。当您点击“确定”或“取消”时,视图是否知道要做什么?当然不知道...但是一旦点击后,面板必须自行消失。相反,如果您创建了一个输入密码的面板,视图将无法进行验证过程。这将是控制器的工作来同意或拒绝提供的密码。
好的,我想您已经理解了MVC,现在是时候谈论让所有这些组件如何一起交流的正确方式了。
视图应该如何通知控制器?实际上这取决于您使用的语言。首先考虑以下几点:
- 避免视图了解控制器。
- 视图不应从控制器获取信息,也不应调用其方法。它如何告诉控制器发生了什么?通过观察者(或侦听器)
小插曲:如果只是为了保持视觉正确,视图可以调用另一个视图的方法。例如,调整窗口大小将确保此窗口的所有组件(子视图)仍然正确显示,但控制器可能不会被通知窗口更改了其大小。
MVC之间的通信是关键!那么它是如何工作的呢?
- 谁将创建和显示视图?控制器!
- 谁应该注册视图事件?也是控制器...在创建后
- 如何?视图的工作是在发生事件时广播它们。通常通过Java中的ActionListener,Objective-C中的观察者,C++中的Lambda函数,JavaScript中的函数回调。控制器通过实现正确的“回调”函数actionPerformed来监听它。
- 因此,当从控制器创建的视图触发事件时,控制器会收到通知并且必须处理此事件。如果控制器想要更改视图中的某些内容,这很容易,因为控制器始终拥有并了解视图。
- 对于每个窗口一个控制器:听起来不错,您不必只有一个控制器(也不需要一个模型或视图...)创建登录控制器对象实例时,该对象可以创建和显示登录窗口。
- 每个服务一个模型实例。这里就涉及到单例设计模式,您可能只需要一个“身份验证模型”,因为您在同一应用程序中只能登录或注销。因此,您实际上只想拥有一个登录模型实例,而不是为每个依赖于登录状态的窗口创建多个登录模型实例。您可以通过登录模型对象的静态实例来实现这一点。这称为单例。
现在我们需要确保将我们要转换为模型的内容复制到所有控制器中。简单情况下是一个控制器和一个模型。控制器更改模型并等待模型被更改,然后使用更改(例如将其显示到视图中)。如果该模型通过多个控制器共享怎么办?例如,该模型是登录服务,您有两种状态:已登录和已注销。有2个控制器正在使用此模型,每个控制器都有自己的视图(窗口),其中一个仅显示密码字段,而第二个是带有图像的窗口,仅在用户登录时显示该图像。对于这种情况没有一种解决方案,而是有几种。一种是使用监听/观察模式。多个控制器可以监听模型的更改。一旦其中之一更改了它,所有控制器都将收到更改的通知,因此将更新视图。另一种方法是保留通知中心(这是另一种模式),并让控制器或模型广播发生的事件,所有对此事件感兴趣的控制器都将通过更改得到通知并处理事件。对于模型可能随时更改的情况,这种最后的解决方案特别有趣(当然是通过某个隐藏的控制器进行更改)。通知中心使用事件循环,并且大多数时间都尝试通知主事件循环(应该是您的UI控制器线程)。
因此,对于我们的示例,如果登录过程是脱机的,则会使用简单的侦听器。2个控制器正在侦听模型更改。在输入密码后,一个控制器将调用方法的模型
login.loginWithPassword(String some password)
,如果密码正确,则模型将广播事件“loginStateChanged”。然后所有侦听器(包括我们的2个控制器)都将收到此事件,并告诉视图使用我们想要的任何内容进行更新(例如显示仅在登录时可以显示的图像)。
在上面的示例中,我们的控制器正在请求模型,而模型直接触发事件,这在控制器的线程内部完成,这是可以的,因为不存在并发访问的风险(相同的线程=没有问题),但是:
如果登录是远程的,例如在线服务认证,我们会敲服务器的门并等待它的回应。由于这个过程可能需要几秒钟,控制器会一直等待服务器的回应。为了避免这种情况,我们应该将请求发送到另一个线程,并修改视图以显示一条消息(正在等待服务器),直到我们从服务器获取答案。一旦我们获得答案(或者如果没有答案,则在超时后),之前创建的线程将获得模型的答案为真或假。非常糟糕的想法是直接使用结果更新视图。问题在于获得答案的线程不在主控制器的线程中运行,因此如果我们更新了视图,则会遇到并发访问(如果视图对象在2个线程中正好同时更改,则会导致崩溃)。确保我们不会干扰主线程的一种方法很简单,只需通过通知将模型的答案发送到主线程,在主事件循环中调度即可。尤其是在这种情况下,我会使用通知中心而不是从模型发送简单广播。有趣的是,一些框架提供了在主事件循环中进行广播的功能。还有更好的解决方案,使用块或lambda函数,但这超出了本问题的范围。
总结如下:
- 视图和模型都不应该知道控制器。
- 只有控制器才知道它们两个。
- 注意它们彼此交流的方式,如果对于同一个模型有多个控制器,使用监听器或通知。
- 过程总是按照以下步骤进行:
- 控制器创建视图并监听它。
- 视图告诉控制器发生了某个操作。
- 控制器处理操作,可能会要求模型更改某些内容。
- 模型返回结果(通过监听器、回调函数或直接返回函数)。
- 控制器处理结果。
- 控制器使用结果更新视图。
authenticated
作为模型状态的一部分,并让视图(s)相应地做出反应。通过模态对话框响应切换登录/注销按钮。 - trashgod