Применение паттерна MVP в классическом ASP.NET

Приходилось ли Вам сталкиваться с долгоиграющими проектами на ASP.NET?
Может быть Вы сейчас над таким проектом как раз и трудитесь?
Если да, то вы скорее всего сталкивались со сложно переплетенным набором событий, логики и валидаций внутри классов страниц.
Эта статья рассказывает о том как можно упростить жизнь на таких проектах используя все тот же шаблон Model-View-Presenter.

Предположу что Вы знакомы с шаблоном MVC и не буду в этой статье на нем подробно останавливаться.

Я попробую сопоставить MVC с теми возможностями которые предоставляет классический ASP.NET:

1. Routing — обработка запросов
Дерево папок сайта в большинстве случаев может служить тем же целям.

2. View и PartialView — представление данных
Класс Page и соответственно UserControl замечательно подходят для этих целей.

3. Model — данные
Классы моделей скорее всего присутствуют в Вашей системе в каком либо виде.

4. Presenter — клей между данными и представлением
Вот этого типа объектов ASP.NET к сожалению не предусматривает.

Реализацией презентера мы сейчас и будем заниматься.
Назначение презентера — подготовить данные для показа пользователю и обработать введенные им данные.
Назначение представления — показать данные пользователю и получить данные от пользователя.

Существуют разные мнения на счет того, насколько умным может быть представление и презентер.
Так как единого мнения нет, то я предпочитаю исходить из эффективности реализации которая зависит от окружения. В данном случае от имеющейся библиотеки ASP.NET которая обладает широким набором достаточно умных элементов управления которые хотелось бы использовать в полной мере, раз уж такая возможность присутствует.

Наконец приступаем к реализации.

Создаем два класса PageView и UserControlView:
public abstract class PageView<T>: Page where T : class
{
    private T _presenter;

    public T Presenter
    {
        get { return _presenter ?? (_presenter = CreatePresenter()); }
    }

    protected abstract T CreatePresenter();
}

public class UserControlView<T> : UserControl
{
    public T Presenter { get; set; }
}

Класс PageView абстрактный и предполагает самостоятельное создание презентера в потомке.
Класс UserControlView обладает единственным свойством — Presenter которое инициализируется родителем на котором пользовательский элемент располагается.

Предполагается что презентер страницы содержит презентеры своих пользовательских элементов.

Пример реализации списка и его элемента:

List.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="List.aspx.cs" Inherits="WebApplication1.Client.List" EnableViewState="false" %>
<%@ Register TagPrefix="ctl" TagName="Item" Src="Details.ascx" %>
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <asp:Repeater ID="ctlList" runat="server" >
        <ItemTemplate>
            <ctl:Item runat="server" Presenter="<%# Container.DataItem %>" />
        </ItemTemplate>
    </asp:Repeater>
</asp:Content>

List.aspx.cs
public partial class List : PageView<ListPresenter>
{
    protected override ListPresenter CreatePresenter()
    {
        return new ListPresenter();
    }

    protected override void OnLoad(System.EventArgs e)
    {
        base.OnLoad(e);
        ctlList.DataSource = Presenter.Items;
        ctlList.DataBind();
    }
}

Details.aspx
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Details.ascx.cs" Inherits="WebApplication1.Client.Details" %>
<asp:TextBox runat="server" Text="<%# Presenter.Model.FirstName %>" OnTextChanged="OnFirstNameChanged" />
<asp:TextBox runat="server" Text="<%# Presenter.Model.LastName %>" OnTextChanged="OnLastNameChanged" />
<asp:Button runat="server" Text="Save" OnClick="OnSave" />
<br />

Detalis.aspx.cs
public partial class Details : UserControlView<DetailsPresenter>
{
    protected void OnFirstNameChanged(object sender, System.EventArgs e)
    {
        Presenter.Model.FirstName = ((TextBox) sender).Text;
    }

    protected void OnLastNameChanged(object sender, System.EventArgs e)
    {
        Presenter.Model.LastName = ((TextBox)sender).Text;
    }

    protected void OnSave(object sender, System.EventArgs e)
    {
        Presenter.Save();
    }
}


Кстати, прошу заметить, ViewState на страницах выключен.

Полностью пример можно скачать здесь.

Достоинства такого подхода:
1. Код страницы становится значительно проще
2. Можно писать модульные тесты на классы презентеров
3. Можно использовать презентеры для реализации различных интерфейсов (web, win, mobile)
4. Можно использовать совместно с лапшекодом и модернизировать приложение постепенно

Надеюсь мой опыт будет Вам полезен.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 24
  • +2
    как можно упростить жизнь на таких проектах используя все тот же шаблон Model-View-Controller

    вы только усложните жизнь тем, кто будет поддерживать подобный проект. не нужно искать себе преключений на ровном месте.
    если вы только начинаете работать над проектом и вам пришла в голову мысль сдалть Webforms, но с MVC — нет никаких причин этим заниматься — берите ASP.NET MVC и вперед.
    если же вы собралиь переписать какой-нибудь старый проект — то тем более не стоит этого делать, скорее всего получите спагеттиобразный код.
    я, к сожалению, сталкивался в своей практике с подобными решениями — это было ужасно.
    • 0
      Есть еще третий случай — когда есть старый проект с сотнями вебформ, написанный многими поколениями разработчиков. Его довольно бессмысленно уже переписывать на MVC, но новую функциональность добавлять приходится постоянно. И тут подходы в духе описанного очень помогают хотя бы часть проекта написать более-менее нормально и худо-бедно покрыть тестами.
      Идея не нова, у gaidar и XaocCPS в их книжке по MVC что-то подобное, например, описано.
      • 0
        именно для этих целей шаблон и применяется, как в первом абзаце и написано.
        • –1
          я вам больше скажу — об этом подходе то ли Гаттри, то ли Хансельманн писал.
          если вы часть проекта напишите в одном стиле, а часть в другом — то получите зоопарк, с которым еще намучаетесь на поддержке. «в армии все должно быть безобразно, но единообразно» — не нужно стараться сделать хоть что-то «более менее нормально», обычно это только усугубляет ситуацию.
          • –1
            Правильно ли я понимаю - Вы считаете что лучше переписать все,
            чем позволить временно существовать двум шаблонам?
            К сожалению чаще всего получить одобрение бизнеса на такое масштабное изменение
            достаточно проблематично.
            Да и достаточно рискованно. Потому как скорее всего попросят дать оценку,
            а оценить такое масштабное изменение во первых сложно,
            во вторых цифра получится большая и ее придется отстаивать,
            а в третьих велика вероятность ошибки и за нее придется отвечать.
            Я понимаю что есть лучшие практики, и хорошо представляю себе что такое рефакторинг.
            В больших системах иногда приходится мириться с временным существованием двух решений.
            Как ни прискорбно это сознавать.
            И конечно-же от таких временных ситуаций надо избавляться как можно быстрее.
            • –1
              я за единообразие в проекте. и за чистый код.
              • –1
                В этом мы с Вами сходимся.
                А на мой вопрос Вы к сожалению не ответили.
                • –1
                  Вы считаете что лучше переписать все,
                  чем позволить временно существовать двум шаблонам?

                  Насколько временно? все от проекта зависит, но в большинстве случаев я бы рекомендовал либо переписать все на ASP.NET MVC, либо вообще не прибегать к шаблону MVC.
                  Поделитесь опытом такого «временного» существования двух шаблонов — как долго это было и к каким результатам привело.
                  • –1
                    на счет переписать все я высказал свои аргументы выше. есть еще варианты?

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

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

                    Вы сказали что можно вынести логику страницы не используя MVC.
                    Приведите пожалуйста пример.
                    • –1
                      то есть без MVC вы логику вне codebehind класса представить не можете? Очень странно. вы разбиваете свои приложения на слои? Business Logic Layer, Data Access Layer etc. — например?
                      • –1
                        почему же, я могу себе даже представить довольно сложную страницу которая объединяет в себе несколько бизнес сущностей и эта логика специфична для страницы. т.е. не должна быть в доменной области приложения.
                        и я бы хотел эту логику вынести из code-behind и структурировать ее так чтобы можно было части этой страницы использовать повторно.
                        предложите Ваше решение пожалуйста.
                        • –1
                          в доменном слое вообще никакой логики не должно быть.
                          если вам нужны какие-либо части этой страницы как контролы — User или Custom Controls — ваш выбор. если остальную логику можно вынести в бизнес слой. часть логики, часто в таких случаях остается в code behind классе, но минимальная часть — именно спецефичная для вашей страницы. если вы ее где-то еще хотите использовать, то можно сделать базовую страницу и наследоваться от нее.
                          • –1
                            Вы на вопрос мой не ответили. Прочитайте пожалуйста внимательно.
                            • –1
                              не вижу ни одного вопроса, на который бы я не ответил. повторите, пожалуйста, если не трудно — какой именно.
                              на мой взгляд, применение шаблона MVP (вижу, вы все-таки отредактировали это в статье) для Webforms приложений в большинстве случаев неоправданно, вы считаете иначе — думаю, друг друга мы тут не переубедим.
                              • –1
                                и я бы хотел эту логику вынести из code-behind и структурировать ее так чтобы можно было части этой страницы использовать повторно.
                                предложите Ваше решение пожалуйста.
                                я хочу отделить логику страницы от событий и биндинга данных.
                                хочу видеть в codebehind только DataBind и обработку событий.
                                логику страницы хочу видеть отдельно.
                                под логикой страницы я понимаю:
                                — загрузку словарей
                                — подготовку данных для вывода пользователю
                                — хранение используемых моделей
                                — вызовы методов бизнес логики
                                — логика настройки отображения
                                например: если эта модель заполнена, то подгрузить зависимую
                                — взаимодействие между составными частями страницы
                                например: заполнен фильтр -> нажата кнопка поиска -> передаем данные в блок получения результата

                                Для меня это насущная проблема, как и для Eltaron и elw00d
                                Если не использовать подобный описанному подход, то как?

                                Если Вы не видите в этом разделении смысла, можно не отвечать.
            • 0
              То есть вы предлагаете новый функционал писать в старом стиле чуть ли не с бизнес-логикой прямо в обработчиках событий в .aspx.cs-файле? Не, не согласен я. Пусть лучше будет зоопарк — благо он обычно в старых больших проектах и так уже какой-нибудь, да есть :)
              • –1
                бизнес логику вы всегда можете вынести из codebehind-а и без всяких контроллеров.
                я предупреждаю о том, что можно усугубить ситуацию применением MVC шаблона на ровном месте.
        • +1
          На MVC это не очень похоже, скорее MVP.
          • –1
            Если сравнить MVC и MVP - разница между ними только в хранении состояния. MVP частный случай MVC согласно их описанию. Сравнить можно например вот здесь и здесь.
            По этому я предпочел такое название. Вы можете сами выбрать MVC или MVP.
            При этом представленный код будет работать в обоих случаях.
            Для реализации MVP возможно придется добавить сохранение состояния между постбэками.
            • 0
              перечитал, был не прав. это классический MVP
            • 0
              Три с половиной года назад работал в компании, где писал на ASP .NET WebForms. И у нас был свой MVC-фреймворк, написанный нашим гуру тимлидом. На мой взгляд он был достаточно удобен для разработки в WebForms. Если вам интересно, я могу скинуть пару классов в личку, чтобы вы посмотрели, как это было реализовано у нас.
              PS. Я бы предоставил вьюшке доступ к модели напрямую, потому что Presenter.Model.* писать каждый раз это не очень. А Presenter.Save() и другие методы презентера я бы инкапсулировал в некий Action, чтобы вьюшка не зависела от интерфейса презентера. Наоборот, вьюшка должна предоставлять интерфейс ISomeView, и презентер должен иметь ссылку на этот интерфейс. Потому что в противном случае менять реализацию представлений будет сложнее, чем просто реализовать интерфейс ISomeView заново (так как придется учитывать семантику вызовов презентера, и в общем случае вьюшки не будут взаимозаменяемыми).
              • 0
                очень интересно, посмотрю с удовольствием.

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

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

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

                • –1
                  спасибо :) но моя реализация куда проще :)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.