Как структурировать программу C # WinForms Model-View-Presenter (пассивное представление)? - PullRequest
7 голосов
/ 02 декабря 2010

Я разрабатываю графический интерфейс, который имеет следующую базовую идею (аналогично моделированию после базового внешнего вида Visual Studio):

  1. Файловая навигация
  2. Селектор управления (для выбора того, что отображать в компоненте «Редактор»)
  3. Редактор
  4. Регистратор (ошибки, предупреждения, подтверждения и т. Д.)

На данный момент я буду использовать TreeView для навигации по файлам, ListView для выбора элементов управления для отображения в редакторе и RichTextBox для Logger. Редактор будет иметь 2 типа режимов редактирования в зависимости от того, что выбрано в TreeView. Редактор будет либо RichTextBox для ручного редактирования текста внутри файлов, либо панелью с Drag / Drop DataGridViews и вложенными текстовыми ящиками для редактирования в этой панели.

Я пытаюсь следовать шаблону проектирования Passive View для полного отделения Model от View и наоборот. Суть этого проекта заключается в том, что любой компонент, который я добавляю, подлежит редактированию / удалению. Как таковая, мне нужна там независимость от данного контроля к следующему. Если сегодня я использую TreeView для навигации по файлам, но завтра мне говорят использовать что-то еще, я хочу относительно легко реализовать новый элемент управления.

Я просто не понимаю, как структурировать программу. Я понимаю по одному Presenter на элемент управления, но я не знаю, как заставить его работать таким образом, чтобы у меня было представление (весь графический интерфейс программы) с элементами управления (вложенными представлениями), чтобы можно было заменять как ПОЛНОЕ представление, так и отдельные элементы. элементы управления, которые отражают мою модель.

В главном представлении, которое должно быть легким по стандартам пассивного представления, могу ли я реализовывать вложенные представления по отдельности? Если так, скажем, у меня есть интерфейс INavigator для абстрагирования роли объекта Navigator. Навигатору потребуется Презентатор и Модель, чтобы действовать между Представлением Навигатора и Основным Представлением. Я чувствую, что где-то теряюсь в шаблонном жаргоне.

Наиболее похожий вопрос можно найти здесь здесь , но он не отвечает на мой вопрос достаточно подробно.

Кто-нибудь, пожалуйста, помогите мне понять, как "структурировать" эту программу? Я ценю любую помощь.

Спасибо

Daniel

Ответы [ 2 ]

23 голосов
/ 02 декабря 2010

Абстракция - это хорошо, но важно помнить, что в какой-то момент что-то должно знать одну или две вещи о вещи или двух, иначе мы просто будем иметь кучу красивых абстрагированных легос, сидящих на полу вместо того, чтобы их собирали в дом.

Инверсия управления / внедрение зависимостей / flippy-dippy-upppy-down-независимо от того, что мы называем этим на этой неделе контейнер, такой как Autofac , действительно может помочь в этом вместе.

Когда я собираю приложение WinForms, я обычно получаю повторяющийся шаблон.

Я начну с файла Program.cs, который настраивает контейнер Autofac, а затем извлекает из него экземпляр MainForm и показывает MainForm. Некоторые люди называют это оболочкой, рабочим пространством или рабочим столом, но в любом случае это «форма», которая имеет строку меню и отображает либо дочерние окна, либо дочерние пользовательские элементы управления, а когда она закрывается, приложение закрывается.

Далее - вышеупомянутый MainForm. Я делаю базовые вещи, такие как перетаскивание некоторых SplitContainers и MenuBar с и так далее в визуальном конструкторе Visual Studio, а затем начинаю фантазировать в коде: мне будут вводить определенные ключевые интерфейсы в Конструктор MainForm, так что я могу использовать их, чтобы моя MainForm могла управлять дочерними элементами управления без особого знания о них.

Например, у меня может быть интерфейс IEventBroker, который позволяет различным компонентам публиковать или подписываться на «события», такие как BarcodeScanned или ProductSaved. Это позволяет частям приложения реагировать на события в слабосвязанной форме, не полагаясь на подключение традиционных событий .NET. Например, EditProductPresenter, который идет вместе с моим EditProductUserControl, может сказать this.eventBroker.Fire("ProductSaved", new EventArgs<Product>(blah)), а IEventBroker проверит свой список подписчиков на это событие и вызовет их обратные вызовы. Например, ListProductsPresenter может прослушивать это событие и динамически обновлять ListProductsUserControl, к которому оно прикреплено. Конечным результатом является то, что если пользователь сохраняет продукт в одном пользовательском элементе управления, ведущий другого пользовательского элемента управления может реагировать и обновлять себя, если он оказывается открытым, без какого-либо элемента управления, который должен знать о существовании друг друга, и без MainForm необходимость организовать это событие.

Если я разрабатываю приложение MDI, у меня может быть MainForm, реализующий интерфейс IWindowWorkspace, который имеет методы Open() и Close(). Я мог бы внедрить этот интерфейс в мои различные докладчики, чтобы они могли открывать и закрывать дополнительные окна, не зная непосредственно о MainForm. Например, ListProductsPresenter может потребоваться открыть EditProductPresenter и соответствующий EditProductUserControl, когда пользователь дважды щелкает строку в сетке данных в ListProductsUserControl. Он может ссылаться на IWindowWorkspace - что на самом деле является MainForm, но ему не нужно это знать - и вызывать Open(newInstanceOfAnEditControl) и предполагать, что элемент управления был как-то показан в соответствующем месте приложения. (Реализация MainForm, предположительно, поменяет элемент управления где-нибудь на панели.)

Но как, черт возьми, ListProductsPresenter может создать этот экземпляр EditProductUserControl? Фабрики делегатов Autofac - это настоящая радость, поскольку вы можете просто вставить делегата в презентатор, и Autofac автоматически подключит его, как если бы он был фабрикой (псевдокод следует):


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.
    }
}

Таким образом, ListProductsPresenter знает о наборе функций Edit (т. Е. Редакторе презентатора и редактировании пользовательского элемента управления) - и это прекрасно, они идут рука об руку - но это не так необходимо знать обо всех зависимостях набора функций Edit, вместо этого полагаясь на делегата, предоставленного Autofac для разрешения всех этих зависимостей для него.

Как правило, я нахожу, что у меня есть взаимно-однозначное соответствие между «представителем / моделью представления / контролирующим контроллером» (давайте не будем слишком зацикливаться на различиях, поскольку в конце дня они все очень похожи) и "UserControl / Form". UserControl принимает модель / контроллер презентатора / представления в своем конструкторе и привязывает данные в зависимости от обстоятельств, максимально откладывая до презентатора. Некоторые люди скрывают UserControl от докладчика через интерфейс, например IEditProductView, что может быть полезно, если представление не является полностью пассивным. Я склонен использовать привязку данных для всего, поэтому связь осуществляется через INotifyPropertyChanged и не беспокоить.

Но вы сделаете свою жизнь намного проще, если ведущий беззастенчиво привязан к представлению. Разве свойство в вашей объектной модели не связано с привязкой данных? Выставьте новое свойство, так оно и есть. У вас никогда не будет EditProductPresenter и EditProductUserControl с одним макетом, а затем вы захотите написать новую версию пользовательского элемента управления, которая будет работать с тем же докладчиком. Вы просто отредактируете их обоих: они предназначены для всех целей и целей, это одно устройство, одна функция, презентатор существует только потому, что его легко проверить на устройстве, а пользовательский контроль - нет.

Если вы хотите, чтобы функция была заменяемой, вам необходимо абстрагировать всю функцию как таковую. Таким образом, у вас может быть интерфейс INavigationFeature, с которым общается ваш MainForm. Вы можете иметь TreeBasedNavigationPresenter, который реализует INavigationFeature и потребляется TreeBasedUserControl. И у вас может быть CarouselBasedNavigationPresenter, который также реализует INavigationFeature и потребляется CarouselBasedUserControl. Пользовательские элементы управления и докладчики по-прежнему идут рука об руку, но ваш MainForm не будет заботиться о том, взаимодействует ли он с древовидным или карусельным представлениями, и вы можете поменять их без MainForm быть мудрее.

В заключение легко запутаться. Каждый педантичен и использует немного другую терминологию, чтобы передать свои тонкие (и зачастую неважные) различия между похожими архитектурными паттернами. По моему скромному мнению, внедрение зависимостей делает чудеса для создания компонуемых, расширяемых приложений, так как связь не поддерживается; разделение функций на «презентаторы / модели представлений / контроллеры» и «представления / пользовательские элементы управления / формы» создает чудеса качества, поскольку большая часть логики перенесена в первую, что позволяет легко тестировать модули; и объединение двух принципов, кажется, действительно то, что вы ищете, вы просто запутались в терминологии.

Или я мог бы быть полон этого. Удачи!

4 голосов
/ 16 сентября 2012

Я знаю, что этому вопросу уже почти 2 года, но я нахожусь в очень похожей ситуации.Как и вы, я искал Интернет в течение ДНЕЙ и не нашел конкретного примера, который бы соответствовал моим потребностям - чем больше я искал, тем больше я продолжал возвращаться на одни и те же сайты снова и снова до такой степени, что у меня было около 10 страниц фиолетового цвета.ссылки в Google!

В любом случае, мне было интересно, нашли ли вы когда-нибудь удовлетворительное решение проблемы?Я опишу, как я проделал это до сих пор, основываясь на том, что я прочитал за последнюю неделю:

Моими целями были: пассивная форма, сначала докладчик (докладчик создает экземпляр формы, поэтому форма не имеетзнание презентатора) Вызывайте методы в презентаторе, вызывая события в форме (представление)

Приложение имеет один FormMain, который содержит 2 пользовательских элемента управления:

ControlsView (имеет 3 кнопки) DocumentView(Сторонний просмотрщик миниатюр изображений)

«Основная форма» содержит панель инструментов для обычного сохранения файлов и т. Д. И чего-то еще.Пользовательский элемент управления «ControlsView» позволяет пользователю нажимать «Сканирование документов». Он также содержит элемент управления древовидной структуры для отображения иерархии документов и страниц. «DocumentView» показывает эскизы отсканированных документов

Мне это действительно показалосьчто каждый элемент управления должен иметь свою собственную триаду MVP, а также основную форму, но я хотел, чтобы все они ссылались на одну и ту же модель.Я просто не мог понять, как координировать связь между элементами управления.

Например, когда пользователь нажимает «Сканирование», ControlsPresenter берет на себя ответственность за получение изображений со сканера, и я хотел, чтобы ондобавьте страницу в древовидную структуру, так как каждая страница возвращается со сканера - нет проблем, - но я также хотел, чтобы миниатюра появлялась в DocumentsView одновременно (проблема, поскольку докладчики не знали друг о друге).

Мое решение состояло в том, чтобы ControlsPresenter вызывал метод в модели, чтобы добавить новую страницу в бизнес-объект, а затем в модели я вызываю событие «PageAdded».

Затем у меня есть оба ControlsPresenterи DocumentPresenter «прослушивает» это событие, так что ControlsPesenter сообщает своему представлению добавить новую страницу в древовидную структуру, а DocumentPresenter сообщает своему представлению о добавлении нового эскиза.

Подводя итог:

Представление элементов управления - вызывает событие «ScanButtonClicked»

Элемент управления Presenter - слышит событие, вызывает класс Scanner к AcquireImages следующим образом:

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                
}

Когда сканируется каждая страница, цикл сканирования вызывает «yield return new Page (PageID)».Вышеуказанный метод вызывает m_DocumentModel.AddPage (page).Новая страница добавляется в модель, которая вызывает событие.И управляющий, и презентатор документов «слышат» событие и соответственно добавляют элементы.

Бит, в котором я не «уверен», - это инициализация всех докладчиков - я делаю это в Program.cs какследует:

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);                                                         
}

Не уверен, хорошо это, плохо или безразлично!

В любом случае, что за огромный пост на двухлетнем вопросе - будьте добры, чтобы получить некоторую обратную связь, хотя.

...