问题
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
回答1:
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 the MainForm
from it, and shows the MainForm
. 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 some SplitContainers
and MenuBar
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 the MainForm
'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" like BarcodeScanned
or ProductSaved
. 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, the EditProductPresenter
that goes along with my EditProductUserControl
could say this.eventBroker.Fire("ProductSaved", new EventArgs<Product>(blah))
and the IEventBroker
would check its list of subscribers for that event and call their callbacks. For example, the ListProductsPresenter
could listen for that event and dynamically update the ListProductsUserControl
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 the MainForm
having to orchestrate that event.
If I'm designing an MDI application, I might have the MainForm
implement an IWindowWorkspace
interface that has Open()
and Close()
methods. I could inject that interface into my various presenters to allow them to open and close additional windows without them being aware of the MainForm
directly. For example, the ListProductsPresenter
might want to open an EditProductPresenter
and corresponding EditProductUserControl
when the user double-clicks a row in a data grid in a ListProductsUserControl
. It can reference an IWindowWorkspace
--which is actually the MainForm
, but it doesn't need to know that--and call Open(newInstanceOfAnEditControl)
and assume that the control was shown in the appropriate place of the application somehow. (The MainForm
implementation would, presumably, swap the control into view on a panel somewhere.)
But how the hell would the ListProductsPresenter
create that instance of the EditProductUserControl
? 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):
public class EditProductUserControl : UserControl
{
public EditProductUserControl(EditProductPresenter presenter)
{
// initialize databindings based on properties of the presenter
}
}
public class EditProductPresenter
{
// Autofac will do some magic when it sees this injected anywhere
public delegate EditProductPresenter Factory(int productId);
public EditProductPresenter(
ISession session, // The NHibernate session reference
IEventBroker eventBroker,
int productId) // An optional product identifier
{
// do stuff....
}
public void Save()
{
// do stuff...
this.eventBroker.Publish("ProductSaved", new EventArgs(this.product));
}
}
public class ListProductsPresenter
{
private IEventBroker eventBroker;
private EditProductsPresenter.Factory factory;
private IWindowWorkspace workspace;
public ListProductsPresenter(
IEventBroker eventBroker,
EditProductsPresenter.Factory factory,
IWindowWorkspace workspace)
{
this.eventBroker = eventBroker;
this.factory = factory;
this.workspace = workspace;
this.eventBroker.Subscribe("ProductSaved", this.WhenProductSaved);
}
public void WhenDataGridRowDoubleClicked(int productId)
{
var editPresenter = this.factory(productId);
var editControl = new EditProductUserControl(editPresenter);
this.workspace.Open(editControl);
}
public void WhenProductSaved(object sender, EventArgs e)
{
// refresh the data grid, etc.
}
}
So the ListProductsPresenter
knows about the Edit
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 the Edit
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
". The UserControl
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 the UserControl
from the presenter via an interface, like IEditProductView
, which can be useful if the view is not completely passive. I tend to use databinding for everything so the communication is done via INotifyPropertyChanged
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 an EditProductUserControl
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 your MainForm
talks to. You can have a TreeBasedNavigationPresenter
that implements INavigationFeature
and is consumed by a TreeBasedUserControl
. And you might have a CarouselBasedNavigationPresenter
that also implements INavigationFeature
and is consumed by a CarouselBasedUserControl
. The user controls and the presenters still go hand-in-hand, but your MainForm
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 the MainForm
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!
回答2:
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:
GDPictureScanning scanner = new GDPictureScanning();
IEnumerable<Page> pages = scanner.AquireImages();
foreach (Page page in pages)
{
m_DocumentModel.AddPage(page);
//The view gets notified of new pages via events raised by the model
//The events are subscribed to by the various presenters so they can
//update views accordingly
}
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:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
IDocumotiveCaptureView view = new DocumotiveCaptureView();
IDocumentModel model = new DocumentModel();
IDocumotiveCapturePresenter Presenter = new DocumotiveCapturePresenter(view, model);
IControlsPresenter ControlsPresenter = new ControlsPresenter(view.ControlsView, model);
IDocumentPresenter DocumentPresenter = new DocumentPresenter(view.DocumentView, model);
Application.Run((Form)view);
}
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...
来源:https://stackoverflow.com/questions/4329776/how-to-structure-a-c-sharp-winforms-model-view-presenter-passive-view-program