如何构建 C# WinForms 模型-视图-呈现器(被动视图)程序?
我正在设计一个具有以下基本思想的 GUI(类似于 Visual Studio 的基本外观和感觉建模):
- 文件导航
- 控制选择器(用于选择在编辑器组件中显示的内容)
- 编辑器
- 记录器(错误、警告、确认等) .)
现在,我将使用 TreeView 进行文件导航,使用 ListView 来选择要在编辑器中显示的控件,使用 RichTextBox 进行记录器。编辑器将有 2 种类型的编辑模式,具体取决于在 TreeView 中选择的内容。编辑器要么是一个用于手动编辑文件内文本的 RichTextBox,要么是一个带有拖放 DataGridView 和子文本框的面板,用于在此面板中进行编辑。
我试图遵循被动视图设计模式,将模型与视图完全分离,反之亦然。该项目的本质是我添加的任何组件都可以编辑/删除。因此,我需要独立于给定的控制到下一个。如果今天我使用 TreeView 进行文件导航,但明天我被告知使用其他东西,那么我想相对轻松地实现一个新控件。
我根本不明白如何构建该程序。我了解每个控件一个演示者,但我不知道如何使其工作,以便我有一个带有控件(子视图)的视图(程序的整个 GUI),以便整个视图以及单个视图都可以替换反映我的模型的控件。
在按被动视图标准应该是轻量级的主视图中,我是否单独实现子视图?如果是这样,假设我有一个接口 INavigator 来抽象 Navigator 对象的角色。导航器需要一个 Presenter 和一个 Model 来在导航器视图和主视图之间进行操作。我觉得我在某个地方迷失了设计模式术语。
可以在此处找到最相似的问题,但它没有回答我的问题足够详细。
有人可以帮助我理解如何“构建”这个程序吗?我很感激任何帮助。
谢谢,
丹尼尔
I am designing a GUI that has the following basic idea (similarly modeled after Visual Studio's basic look-and-feel):
- File navigation
- Control selector (for selecting what to display in the Editor component)
- Editor
- Logger (errors, warnings, confirmation, etc.)
For now, I will be using a TreeView for file navigation, a ListView for selecting controls to be displayed in the Editor and a RichTextBox for the Logger. The Editor will have 2 types of editing modes depending on what is selected in the TreeView. The Editor will either be a RichTextBox for manually editing text inside files, or it will be a Panel with Drag/Drop DataGridViews and sub-TextBoxes for editing in this Panel.
I am attempting to follow the Passive View design pattern for complete separation of Model from View and vice versa. The nature of this project is that any component I add is subject to edit/removal. As such, I need there to independence from a given control to the next. If today I am using a TreeView for file navigation, but tomorrow I am told to use something else, then I want to implement a new control with relative ease.
I simply do not understand how to structure the program. I understand one Presenter per Control, but I do not know how to make it work such that I have a View (the entire GUI of the program) with controls (sub-Views) such that the ENTIRE View is replaceable as well as the individual controls that reflect my model.
In the main View, which is supposed to be lightweight by Passive View standards, do I implement the sub-Views individually? If so, say I have an interface INavigator to abstract the role of the Navigator object. The navigator will need a Presenter and a Model to act between the Navigator View and the main View. I feel like I am getting lost in the design pattern jargon somewhere.
The most similarly-related question can be found here, but it does not answer my question in sufficient detail.
Will anybody please help me understand how to "structure" this program? I appreciate any help.
Thanks,
Daniel
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
抽象是好的,但重要的是要记住,在某些时候某物必须了解一两件事的一两件事,否则我们只会在上面放一堆精美抽象的乐高积木。地板而不是将它们组装成房子。
控制反转/依赖注入/flippy-dippy-upside-down-whatever-we're-calling-it-this-week 容器,例如 Autofac 确实可以帮助将这一切拼凑在一起。
当我组装一个 WinForms 应用程序时,通常会得到一个重复的模式。
我将从配置 Autofac 容器的
Program.cs
文件开始,然后从中获取MainForm
的实例,并显示MainForm
>。有些人称其为外壳、工作区或桌面,但无论如何它是具有菜单栏并显示子窗口或子用户控件的“表单”,当它关闭时,应用程序退出。接下来是前面提到的
MainForm
。我在 Visual Studio 可视化设计器中执行一些基本操作,例如拖放一些SplitContainers
和MenuBar
等,然后我开始喜欢代码:我'我会将某些关键接口“注入”到 MainForm 的构造函数中,以便我可以使用它们,这样我的 MainForm 就可以编排子控件,而无需真正了解它们。例如,我可能有一个
IEventBroker
接口,允许各种组件发布或订阅“事件”,例如BarcodeScanned
或ProductSaved
。这允许应用程序的各个部分以松散耦合的方式响应事件,而不必依赖连接传统的 .NET 事件。例如,与我的EditProductUserControl
一起使用的EditProductPresenter
可能会显示this.eventBroker.Fire("ProductSaved", new EventArgs(blah))< /code> 和
IEventBroker
将检查该事件的订阅者列表并调用它们的回调。例如,ListProductsPresenter
可以侦听该事件并动态更新它所附加的ListProductsUserControl
。最终结果是,如果用户将产品保存在一个用户控件中,则另一个用户控件的演示者可以做出反应并更新自身(如果它碰巧打开),而任一控件都不必知道彼此的存在,并且无需MainForm 必须协调该事件。
如果我正在设计一个 MDI 应用程序,我可能会让
MainForm
实现一个IWindowWorkspace
接口,该接口具有Open()
和Close( )
方法。我可以将该界面注入到我的各个演示者中,以允许他们打开和关闭其他窗口,而无需直接了解MainForm
。例如,当用户双击ListProductsPresenter
中数据网格中的一行时,ListProductsPresenter
可能想要打开EditProductPresenter
和相应的EditProductUserControl
。代码>ListProductsUserControl。它可以引用IWindowWorkspace
(实际上是MainForm
),但它不需要知道这一点 - 并调用Open(newInstanceOfAnEditControl)
并假设该控件以某种方式显示在应用程序的适当位置。 (MainForm
实现大概会将控件交换到面板上某处的视图中。)但是
ListProductsPresenter
创建 该实例到底是如何的呢?EditProductUserControl
的? Autofac 的委托工厂在这里是一个真正的乐趣,因为您可以将委托注入到Presenter 和 Autofac 会自动将其连接起来,就像它是一个工厂一样(伪代码如下):因此
ListProductsPresenter
了解Edit
功能集(即,编辑 Presenter 和编辑用户控件)——这完全没问题,它们是齐头并进的——但它不需要知道Edit依赖项 code> 功能集,而不是依赖 Autofac 提供的委托来解决它的所有这些依赖关系。
一般来说,我发现“演示者/视图模型/监督控制器”之间存在一对一的对应关系(我们不要太关注差异,因为最终它们都非常相似)和“
用户控件
/表单
”。UserControl
在其构造函数中接受演示者/视图模型/控制器,并根据需要进行数据绑定,尽可能地遵循演示者。有些人通过界面(例如 IEditProductView)向演示者隐藏了UserControl
,如果视图不是完全被动的,这可能会很有用。我倾向于对所有内容使用数据绑定,因此通信是通过 INotifyPropertyChanged 完成的,无需费心。但是,如果演讲者无耻地与观点联系在一起,你的生活就会变得更加轻松。对象模型中的属性是否与数据绑定不匹配?公开一个新属性就是如此。您永远不会拥有具有同一种布局的
EditProductPresenter
和EditProductUserControl
,然后想要编写与同一演示器一起使用的新版本的用户控件。您只需编辑它们,它们的所有意图和目的都是一个单元,一个功能,演示者仅存在,因为它很容易进行单元测试,而用户控件则不然。如果您希望某个功能可替换,则需要将整个功能抽象化。因此,您可能有一个与
MainForm
对话的INavigationFeature
接口。您可以拥有一个实现INavigationFeature
并由TreeBasedUserControl
使用的TreeBasedNavigationPresenter
。您可能有一个CarouselBasedNavigationPresenter
,它也实现INavigationFeature
并由CarouselBasedUserControl
使用。用户控件和演示者仍然齐头并进,但是您的MainForm
不必关心它是否与基于树的视图或基于轮播的视图交互,并且您可以交换它们在MainForm
不知情的情况下就被淘汰了。最后,你很容易让自己感到困惑。每个人都是迂腐的,并使用略有不同的术语来表达相似的架构模式之间的微妙(通常是不重要的)差异。以我的拙见,依赖注入对于构建可组合、可扩展的应用程序确实有奇迹,因为耦合被降低了。将功能分离为“呈现器/视图模型/控制器”和“视图/用户控件/表单”对于质量来说确实是奇迹,因为大多数逻辑都被引入到前者中,从而可以轻松进行单元测试;将这两个原则结合起来似乎确实是您正在寻找的,您只是对术语感到困惑。
或者,我可能已经充满了它。祝你好运!
Abstraction is good, but it's important to remember that at some point something has to know a thing or two about a thing or two, or else we'll just have a pile of nicely abstracted legos sitting on the floor instead of them being assembled into a house.
An inversion-of-control/dependency injection/flippy-dippy-upside-down-whatever-we're-calling-it-this-week container like Autofac can really help in piecing this all together.
When I throw together a WinForms application, I usually end up with a repeating pattern.
I'll start with a
Program.cs
file that configures the Autofac container and then fetches an instance of theMainForm
from it, and shows theMainForm
. Some people call this the shell or the workspace or the desktop but at any rate it's "the form" that has the menu bar and displays either child windows or child user controls, and when it closes, the application exits.Next is the aforementioned
MainForm
. I do the basic stuff like drag-and-dropping someSplitContainers
andMenuBar
s and such in the Visual Studio visual designer, and then I start getting fancy in code: I'll have certain key interfaces "injected" into theMainForm
's constructor so that I can make use of them, so that my MainForm can orchestrate child controls without really having to know that much about them.For example, I might have an
IEventBroker
interface that lets various components publish or subscribe to "events" likeBarcodeScanned
orProductSaved
. This allows parts of the application to respond to events in a loosely coupled way, without having to rely on wiring up traditional .NET events. For example, theEditProductPresenter
that goes along with myEditProductUserControl
could saythis.eventBroker.Fire("ProductSaved", new EventArgs<Product>(blah))
and theIEventBroker
would check its list of subscribers for that event and call their callbacks. For example, theListProductsPresenter
could listen for that event and dynamically update theListProductsUserControl
that it is attached to. The net result is that if a user saves a product in one user control, another user control's presenter can react and update itself if it happens to be open, without either control having to be aware of each other's existence, and without theMainForm
having to orchestrate that event.If I'm designing an MDI application, I might have the
MainForm
implement anIWindowWorkspace
interface that hasOpen()
andClose()
methods. I could inject that interface into my various presenters to allow them to open and close additional windows without them being aware of theMainForm
directly. For example, theListProductsPresenter
might want to open anEditProductPresenter
and correspondingEditProductUserControl
when the user double-clicks a row in a data grid in aListProductsUserControl
. It can reference anIWindowWorkspace
--which is actually theMainForm
, but it doesn't need to know that--and callOpen(newInstanceOfAnEditControl)
and assume that the control was shown in the appropriate place of the application somehow. (TheMainForm
implementation would, presumably, swap the control into view on a panel somewhere.)But how the hell would the
ListProductsPresenter
create that instance of theEditProductUserControl
? Autofac's delegate factories are a true joy here, since you can just inject a delegate into the presenter and Autofac will automagically wire it up as if it were a factory (pseudocode follows):So the
ListProductsPresenter
knows about theEdit
feature set (i.e., the edit presenter and the edit user control)--and this is perfectly fine, they go hand-in-hand--but it doesn't need to know about all of the dependencies of theEdit
feature set, instead relying on a delegate provided by Autofac to resolve all of those dependencies for it.Generally, I find that I have a one-to-one correspondence between a "presenter/view model/supervising controller" (let's not too caught up on the differences as at the end of the day they are all quite similar) and a "
UserControl
/Form
". TheUserControl
accepts the presenter/view model/controller in its constructor and databinds itself as is appropriate, deferring to the presenter as much as possible. Some people hide theUserControl
from the presenter via an interface, likeIEditProductView
, which can be useful if the view is not completely passive. I tend to use databinding for everything so the communication is done viaINotifyPropertyChanged
and don't bother.But, you will make your life much easier if the presenter is shamelessly tied to the view. Does a property in your object model not mesh with databinding? Expose a new property so it does. You are never going to have an
EditProductPresenter
and anEditProductUserControl
with one layout and then want to write a new version of the user control that works with the same presenter. You will just edit them both, they are for all intents and purpose one unit, one feature, the presenter only existing because it is easily unit testable and the user control is not.If you want a feature to be replaceable, you need to abstract the entire feature as such. So you might have an
INavigationFeature
interface that yourMainForm
talks to. You can have aTreeBasedNavigationPresenter
that implementsINavigationFeature
and is consumed by aTreeBasedUserControl
. And you might have aCarouselBasedNavigationPresenter
that also implementsINavigationFeature
and is consumed by aCarouselBasedUserControl
. The user controls and the presenters still go hand-in-hand, but yourMainForm
would not have to care if it is interacting with a tree-based view or a carousel-based one, and you could swap them out without theMainForm
being the wiser.In closing, it is easy to confuse yourself. Everyone is pedantic and uses slightly different terminology to convey they subtle (and oftentimes unimportant) differences between what are similar architectural patterns. In my humble opinion, dependency injection does wonders for building composable, extensible applications, since coupling is kept down; separation of features into "presenters/view models/controllers" and "views/user controls/forms" does wonders for quality since most logic is pulled into the former, allowing it to be easily unit tested; and combining the two principles seems to really be what you're looking for, you're just getting confused on the terminology.
Or, I could be full of it. Good luck!
我知道这个问题已经有近两年的历史了,但我发现自己处于非常相似的情况。和你一样,我在互联网上搜索了好几天,但没有找到适合我需求的具体例子 - 我搜索的越多,我一遍又一遍地回到相同的网站,直到我有大约 10 页紫色的页面谷歌链接!
不管怎样,我想知道你是否想出了一个令人满意的解决方案来解决这个问题?我将根据我上周读到的内容概述到目前为止我是如何做到这一点的:
我的目标是:
被动形式,演示者优先(演示者实例化表单,因此表单不知道其演示者)
通过在表单(视图)中引发事件来调用演示者中的方法
应用程序有一个 FormMain,其中包含 2 个用户控件:
ControlsView(有 3 个按钮)
DocumentView(第 3 方图像缩略图查看器)
“主窗体”包含一个用于常用文件保存内容等的工具栏,仅此而已。
“ControlsView”用户控件允许用户单击“扫描文档”
它还包含一个树视图控件来显示文档和页面的层次结构
“DocumentView”显示扫描文档的缩略图
我真的觉得每个控件都应该有自己的 MVP 三元组以及主窗体,但我希望它们都引用相同的模型。我只是不知道如何协调控件之间的通信。
例如,当用户单击“扫描”时,ControlsPresenter 负责从扫描仪获取图像,我希望它在从扫描仪返回的每个页面时将页面添加到树视图中 - 没问题 - 但我也想要缩略图同时出现在 DocumentsView 中(问题是演示者彼此不认识)。
我的解决方案是让 ControlsPresenter 调用模型中的方法以将新页面添加到业务对象中,然后在模型中引发“PageAdded”事件。
然后,我让 ControlsPresenter 和 DocumentPresenter “监听”此事件,以便 ControlsPesenter 告诉它的视图将新页面添加到树视图,而 DocumentPresenter 告诉它的视图添加新的缩略图。
总结一下:
控制视图 - 引发事件“ScanButtonClicked”
控制演示者 - 听到该事件,调用 Scanner 类来 AcquireImages,如下所示:
扫描每个页面时,扫描循环调用“yield return new Page(PageID)”。
上述方法调用m_DocumentModel.AddPage(page)。
新页面将添加到模型中,从而引发一个事件。
两者都控制演示者和文档演示者“听到”事件并相应地添加项目。
我不太“确定”的是所有演示者的初始化 - 我在 Program.cs 中执行此操作,如下所示:
不确定这是好、坏还是无关紧要!
不管怎样,关于一个两年前的问题的一篇文章是多么巨大——不过最好能得到一些反馈......
I know this question is nearly 2 years old but I find myself in a very similar situation. Like you, I have scoured the internet for DAYS and not found a concrete example that fits my needs - the more I searched the more I kept coming back to the same sites over and over again to the point where I had about 10 pages of purple links in Google!
Anyway, I was wondering if you ever came up with a satisfactory solution to the problem? I'll outline how I have gone about it so far, based on what I have read over the last week:
My aims were:
Passive form, presenter first (the presenter instantiates the form so the form has no knowledge of it's presenter)
Call methods in the presenter by raising events in the form (view)
The application has a single FormMain which contains 2 user controls:
ControlsView (has 3 buttons)
DocumentView (A 3rd party image thumbnail viewer)
The "Main Form" holds a toolbar for the usual file save stuff etc. and little else.
The "ControlsView" user control allows the user to click "Scan Documents"
It also contains a treeview control to display a hierarchy of documents and pages
The "DocumentView" shows thumbnails of the scanned documents
It really felt to me that each control should have it's own MVP triad, as well as the main form, but I wanted them all to reference the same model. I just could not work out how to co-ordinate the communication between the controls.
For example, when the user clicks "Scan", the ControlsPresenter takes charge of acquiring the images from the scanner and I wanted it to add the page to the treeview as each page returned from the scanner - no problem - but I also wanted the thumbnail to appear in the DocumentsView at the same time (problem as the presenters don't know about each other).
My solution was for the ControlsPresenter to call a method in the model to add the new page into the business object, and then in the model I raise a "PageAdded" event.
I then have both the ControlsPresenter and the DocumentPresenter "listening" to this event so that the ControlsPesenter tells it's view to add the new page to the treeview, and the DocumentPresenter tells it's view to add the new thumbnail.
To summarise:
Controls View - raises event "ScanButtonClicked"
Controls Presenter - hears the event, calls Scanner class to AcquireImages as follows:
As each page is scanned, the scanning loop calls a "yield return new Page(PageID)".
The above method calls m_DocumentModel.AddPage(page).
The new page is added to the model, which raises an event.
Both controls presenter and document presenter "hear" the event and add items accordingly.
The bit I'm not "sure" about is the initialisation of all the presenters - I'm doing this within Program.cs as follows:
Not sure if this is good, bad or indifferent!
Anyway, what a huge post on a two year old question - be good to get some feedback though...