RONUA
ROmanian .NET User Association --- Asociaţia Romană a Utilizatorilor .NET
Comunitatea dezvoltatorilor software pe .NET Framework
WebForms MVP. Ce este? Cum se folosește?

În cadrul ultimului articol am făcut o prezentare de anti-utilizare a tehnologiei ASP.NET. Există însă și modele de dezvoltare care ușurează munca dezvoltatorului. Un astfel de exemplu este WebFormsMVP un framework construit cu ASP.NET WebForms care utilizează Model-View-Presenter ca să ofere practici precum „Separation Of Concerns”, „Do not Repeat Yourself”, „Unit testing” pe lângă care mai adăugăm și ușurința dezvoltării în ASP.NET WebForms.

Framework-ul a fost dezvoltat de o „gașcă” de MVPs (Most-Valued-Professionals); între noi fiind vorba sunt cam multe prescurtări în industria noastra (TDD,IOC,DI,DIP,LSP,SRP,MVP,MVC,ETC). Dorința lor e de a mai modera impactul major pe care ASP.NET MVC îl exercită asupra comunității. E ușor să spui că scriind codul în ASP.NET MVC acel cod e mai ușor de întreținut ( să vă văd cum îl întrețineți când aveți constructori cu 20+ parameteri, poate în altă postare pe blog ); însă din ce vă voi prezenta sper să reliefez că poți scrie cod testabil chiar și pe ASP.NET WebForms.

Pentru exemplul curent am downloadat changeset-ul 36125, care dacă e să dăm crezare comentariului ar reprezenta version 0.9.7.0. Deși încă în faza de CTP, băieții spun că framework-ul este stabil și folosit deja în producție.

Întrucât nu sunt pe deplin familiarizat cu acest cadru de dezvoltare, am să mă limitez la ce puteți observa în soluția WebFormsMVP.sln pe care o puteți regăsi în trunk.

Configurarea IoC-ului.

Deși inițial ofereau suport doar pentru Windsor Container, la cererea comunității au adăugat și suport pentru Unity. În Global.asax o să găsiți configurarea acestuia în felul următor:

Code Snippet
  1. protected void Application_Start(object sender, EventArgs e)
  2.         {
  3.             PresenterBinder.Factory = new UnityPresenterFactory(build=>
  4.                                                                     {
  5.                                                                         build.RegisterType<IAmInjected, AmInjected>(
  6.                                                                             new Microsoft.Practices.Unity.ContainerControlledLifetimeManager());
  7.                                                                         build.RegisterType<HelloWorldPresenter>(
  8.                                                                             new TransientLifetimeManager());
  9.                                                                     });
  10.         }

Practice-ul pe care în folosesc adesea când vine vorba de Unity este să creez clase statice pentru ceea ce doresc să configurez similar cu:

Code Snippet
  1. internal class ScriptsServicesBuilder
  2.     {
  3.         
  4.         public static void Build(IUnityContainer container)
  5.         {
  6.             
  7.             // register javascript cacher
  8.             container
  9.                 .RegisterType<IScriptCompressionService, MicrosoftAjaxMinifierCompressionService>()
  10.                 .RegisterType<IScriptsResolverService, ScriptsConfigurationSectionResolverService>(JS_CONFIGURATION_RESOLVER_SERVICE,
  11.                 new ContainerControlledLifetimeManager(),
  12.                 new InjectionConstructor(
  13.                     new ResolvedParameter<IScriptCompressionService>()
  14.                     )
  15.                 )
  16.                 .RegisterType<IScriptsResolverService, CachedScriptsResolverService>(
  17.                 new ContainerControlledLifetimeManager(),
  18.                 new InjectionConstructor(
  19.                     new ResolvedParameter<IScriptsResolverService>(JS_CONFIGURATION_RESOLVER_SERVICE),
  20.                     new ResolvedParameter<ICacheManager>(SCRIPTS_CACHE_MANAGER),
  21.                     lExpiration
  22.                     )
  23.                 );
  24.         }
  25.     }

Iar în builder putem configura ceva în genul

Code Snippet
  1. PresenterBinder.Factory = new UnityPresenterFactory(build=>
  2.                                                                     {
  3.                                                                         ScriptsServicesBuilder.Build(build);
  4.                                                                         build.RegisterType<IAmInjected, AmInjected>(
  5.                                                                             new Microsoft.Practices.Unity.ContainerControlledLifetimeManager());
  6.                                                                         build.RegisterType<HelloWorldPresenter>(
  7.                                                                             new TransientLifetimeManager());
  8.                                                                     });

 

Definirea unui Presenter

Code Snippet
  1. public class HelloWorldPresenter
  2.         : Presenter<IView<HelloWorldModel>>
  3.     {
  4.         readonly IAmInjected amInjected;
  5.  
  6.         public HelloWorldPresenter(IView<HelloWorldModel> view, IAmInjected amInjected)
  7.             : base(view)
  8.         {
  9.             this.amInjected = amInjected;
  10.             View.Load += View_Load;
  11.         }
  12.  
  13.         public override void ReleaseView()
  14.         {
  15.             View.Load -= View_Load;
  16.         }
  17.  
  18.         void View_Load(object sender, EventArgs e)
  19.         {
  20.             SetMessage();
  21.         }
  22.  
  23.         private void SetMessage()
  24.         {
  25.             View.Model.Message = HttpContext.User.Identity.IsAuthenticated
  26.                 ? String.Format("Hello {0}!", HttpContext.User.Identity.Name)
  27.                 : "Hello World!";
  28.         }
  29.     }

Se extinde clasa abstractă WebFormsMvp.Presenter și se declara o intrefața pe care view-ul o implementează

Definirea unui View.

Partea .aspx arată în felul următor

Code Snippet
  1. <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="HelloWorldControl.ascx.cs" Inherits="WebFormsMvp.FeatureDemos.Web.Controls.HelloWorldControl" %>
  2. <div class="hello-world">
  3.     <%# Model.Message %>
  4. </div>

Iar clasa

Code Snippet
  1. using System;
  2. using WebFormsMvp.FeatureDemos.Logic.Presenters;
  3. using WebFormsMvp.FeatureDemos.Logic.Views.Models;
  4. using WebFormsMvp.Web;
  5.  
  6. namespace WebFormsMvp.FeatureDemos.Web.Controls
  7. {
  8.     [PresenterBinding(typeof(HelloWorldPresenter))]
  9.     public partial class HelloWorldControl : MvpUserControl<HelloWorldModel>
  10.     {
  11.     }
  12. }

Foarte similar cu modelul din ASP.NET MVC, diferența majoră o vom observa în view-uri mai complexe precum

Code Snippet
  1. <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LookupWidgetControl.ascx.cs" Inherits="WebFormsMvp.FeatureDemos.Web.Controls.LookupWidgetControl" %>
  2. <div class="lookup-widget">
  3.   <fieldset><legend>Enter ID or Name of widget</legend>
  4.     <ol>
  5.       <li>
  6.         <asp:Label runat="server" AssociatedControlID="widgetId">ID: </asp:Label>
  7.         <asp:TextBox runat="server" ID="widgetId" MaxLength="9" />
  8.         <div class="validators">
  9.           <asp:CompareValidator runat="server" ControlToValidate="widgetId"
  10.             ValidationGroup="LookupWidget"
  11.             Display="Dynamic" Type="Integer" Operator="DataTypeCheck"
  12.             ErrorMessage="ID must be a valid whole number" />
  13.           <asp:RangeValidator runat="server" ControlToValidate="widgetId"
  14.             ValidationGroup="LookupWidget"
  15.             Display="Dynamic" Type="Integer" MinimumValue="1" MaximumValue="9999999"
  16.             ErrorMessage="ID must be a positive whole number" />
  17.         </div>
  18.       </li>
  19.       <li>
  20.         <asp:Label runat="server" AssociatedControlID="widgetName">Name: </asp:Label>
  21.         <asp:TextBox runat="server" ID="widgetName" MaxLength="255" />
  22.       </li>
  23.     </ol>
  24.     <p>
  25.       <asp:Button runat="server" Text="Find" ValidationGroup="LookupWidget"
  26.         OnClick="Find_Click" />
  27.     </p>
  28.   </fieldset>
  29.   <div class="results">
  30.     <asp:DetailsView ID="results" runat="server" DataSource="<%# Model.Widgets %>"
  31.       EmptyDataText="No matching results found"
  32.       Visible="<%# Model.ShowResults %>" />
  33.   </div>
  34. </div>

iar partea de view e responsabilă de transmiterea evenimentelor presenter-ului, acesta fiind responsabil de apelarea serviciilor și setarea valorilor din model pe view:

Code Snippet
  1.  
  2. namespace WebFormsMvp.FeatureDemos.Web.Controls
  3. {
  4.     public partial class LookupWidgetControl
  5.         : MvpUserControl<LookupWidgetModel>, ILookupWidgetView
  6.     {
  7.         protected void Find_Click(object sender, EventArgs e)
  8.         {
  9.             int? id = String.IsNullOrEmpty(widgetId.Text) ?
  10.                 null : id = Convert.ToInt32(widgetId.Text);
  11.             OnFinding(id, widgetName.Text);
  12.         }
  13.  
  14.         public event EventHandler<FindingWidgetEventArgs> Finding;
  15.         private void OnFinding(int? id, string name)
  16.         {
  17.             if (Finding != null)
  18.             {
  19.                 Finding(this, new FindingWidgetEventArgs() { Id = id, Name = name });
  20.             }
  21.         }
  22.     }
  23. }

Logica din presenter este:

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using WebFormsMvp.FeatureDemos.Logic.Data;
  4. using WebFormsMvp.FeatureDemos.Logic.Views;
  5.  
  6. namespace WebFormsMvp.FeatureDemos.Logic.Presenters
  7. {
  8.     public class LookupWidgetPresenter
  9.         : Presenter<ILookupWidgetView>
  10.     {
  11.         private readonly IWidgetRepository widgetRepository;
  12.  
  13.         public LookupWidgetPresenter(ILookupWidgetView view)
  14.             : this(view, null)
  15.         { }
  16.  
  17.         public LookupWidgetPresenter(ILookupWidgetView view, IWidgetRepository widgetRepository)
  18.             : base(view)
  19.         {
  20.             this.widgetRepository = widgetRepository ?? new WidgetRepository();
  21.             View.Finding += View_Finding;
  22.             View.Model.Widgets = new List<Widget>();
  23.         }
  24.  
  25.         public override void ReleaseView()
  26.         {
  27.             View.Finding -= View_Finding;
  28.         }
  29.  
  30.         void View_Finding(object sender, FindingWidgetEventArgs e)
  31.         {
  32.             if ((!e.Id.HasValue || e.Id <= 0) && String.IsNullOrEmpty(e.Name))
  33.                 return;
  34.  
  35.             if (e.Id.HasValue && e.Id > 0)
  36.             {
  37.                 AsyncManager.RegisterAsyncTask(
  38.                     (asyncSender, ea, callback, state) => // Begin
  39.                     {
  40.                         return widgetRepository.BeginFind(e.Id.Value, callback, state);
  41.                     },
  42.                     result => // End
  43.                     {
  44.                         var widget = widgetRepository.EndFind(result);
  45.                         if (widget != null)
  46.                         {
  47.                             View.Model.Widgets.Add(widget);
  48.                         }
  49.                     },
  50.                     result => { } // Timeout
  51.                     , null, false);
  52.             }
  53.             else
  54.             {
  55.                 AsyncManager.RegisterAsyncTask(
  56.                     (asyncSender, ea, callback, state) => // Begin
  57.                     {
  58.                         return widgetRepository.BeginFindByName(e.Name, callback, state);
  59.                     },
  60.                     result => // End
  61.                     {
  62.                         var widget = widgetRepository.EndFindByName(result);
  63.                         if (widget != null)
  64.                         {
  65.                             View.Model.Widgets.Add(widget);
  66.                         }
  67.                     },
  68.                     result => { } // Timeout
  69.                     , null, false);
  70.             }
  71.             AsyncManager.ExecuteRegisteredAsyncTasks();
  72.             View.Model.ShowResults = true;
  73.         }
  74.     }
  75. }

 

 

Testarea presenter-ului.

Code Snippet
  1. using System;
  2. using System.Linq;
  3. using Microsoft.VisualStudio.TestTools.UnitTesting;
  4. using WebFormsMvp.FeatureDemos.Logic.Presenters;
  5. using Rhino.Mocks;
  6. using WebFormsMvp.FeatureDemos.Logic.Views;
  7. using WebFormsMvp.FeatureDemos.Logic.Data;
  8. using WebFormsMvp.Testing;
  9.  
  10. namespace WebFormsMvp.FeatureDemos.UnitTests
  11. {
  12.     [TestClass]
  13.     public class LookupWidgetPresenterTests
  14.     {
  15.         [TestMethod]
  16.         public void LookupWidgetPresenterLoadsWidgetFromId()
  17.         {
  18.             // Arrange
  19.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  20.             var asyncManager = new TestAsyncTaskManager();
  21.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  22.             var widget = new Widget {Id = 1, Name = "Test"};
  23.  
  24.             widgetRepository.Stub(w => w.BeginFind(1, null, null)).IgnoreArguments()
  25.                 .ExecuteAsyncCallback().Return(null);
  26.             widgetRepository.Stub(w => w.EndFind(null)).IgnoreArguments()
  27.                 .Return(widget);
  28.  
  29.             var presenter = new LookupWidgetPresenter(view, widgetRepository)
  30.             {
  31.                 AsyncManager = asyncManager
  32.             };
  33.  
  34.             // Act
  35.             view.Raise(v => v.Load += null, view, new EventArgs());
  36.             view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = 1 });
  37.             asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
  38.             presenter.ReleaseView();
  39.  
  40.             // Assert
  41.             Assert.AreEqual(widget, view.Model.Widgets.First());
  42.         }
  43.  
  44.         [TestMethod]
  45.         public void LookupWidgetPresenterLoadsWidgetFromIdWhenBothIdAndNameSet()
  46.         {
  47.             // Arrange
  48.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  49.             var asyncManager = new TestAsyncTaskManager();
  50.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  51.             var widget = new Widget { Id = 1, Name = "Test" };
  52.  
  53.             widgetRepository.Stub(w => w.BeginFind(1, null, null)).IgnoreArguments()
  54.                 .ExecuteAsyncCallback().Return(null);
  55.             widgetRepository.Stub(w => w.EndFind(null)).IgnoreArguments()
  56.                 .Return(widget);
  57.  
  58.             var presenter = new LookupWidgetPresenter(view, widgetRepository)
  59.             {
  60.                 AsyncManager = asyncManager
  61.             };
  62.  
  63.             // Act
  64.             view.Raise(v => v.Load += null, view, new EventArgs());
  65.             view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = 1, Name = "Blah" });
  66.             asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
  67.             presenter.ReleaseView();
  68.  
  69.             // Assert
  70.             Assert.AreEqual(widget, view.Model.Widgets.First());
  71.         }
  72.  
  73.         [TestMethod]
  74.         public void LookupWidgetPresenterLoadsWidgetFromName()
  75.         {
  76.             // Arrange
  77.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  78.             var asyncManager = new TestAsyncTaskManager();
  79.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  80.             var widget = new Widget {Id = 1, Name = "Test"};
  81.  
  82.             widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
  83.                 .ExecuteAsyncCallback().Return(null);
  84.             widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
  85.                 .Return(widget);
  86.  
  87.             var presenter = new LookupWidgetPresenter(view, widgetRepository)
  88.             {
  89.                 AsyncManager = asyncManager
  90.             };
  91.  
  92.             // Act
  93.             view.Raise(v => v.Load += null, view, new EventArgs());
  94.             view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Name = "Test" });
  95.             asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
  96.             presenter.ReleaseView();
  97.  
  98.             // Assert
  99.             Assert.AreEqual(widget, view.Model.Widgets.First());
  100.         }
  101.  
  102.         [TestMethod]
  103.         public void LookupWidgetPresenterLoadsWidgetFromNameWhenBothIdAndNameSetButIdIsInvalid()
  104.         {
  105.             // Arrange
  106.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  107.             var asyncManager = new TestAsyncTaskManager();
  108.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  109.             var widget = new Widget { Id = 1, Name = "Test" };
  110.  
  111.             widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
  112.                 .ExecuteAsyncCallback().Return(null);
  113.             widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
  114.                 .Return(widget);
  115.  
  116.             var presenter = new LookupWidgetPresenter(view, widgetRepository)
  117.             {
  118.                 AsyncManager = asyncManager
  119.             };
  120.  
  121.             // Act
  122.             view.Raise(v => v.Load += null, view, new EventArgs());
  123.             view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = -1, Name = "Test" });
  124.             asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
  125.             presenter.ReleaseView();
  126.  
  127.             // Assert
  128.             Assert.AreEqual(widget, view.Model.Widgets.First());
  129.         }
  130.  
  131.         [TestMethod]
  132.         public void LookupWidgetPresenterHidesResultsOnInitialLoad()
  133.         {
  134.             // Arrange
  135.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  136.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  137.  
  138.             var presenter = new LookupWidgetPresenter(view, widgetRepository);
  139.  
  140.             // Act
  141.             view.Raise(v => v.Load += null, view, new EventArgs());
  142.             presenter.ReleaseView();
  143.  
  144.             // Assert
  145.             Assert.AreEqual(false, view.Model.ShowResults);
  146.         }
  147.  
  148.         [TestMethod]
  149.         public void LookupWidgetPresenterShowsResultsOnFinding()
  150.         {
  151.             // Arrange
  152.             var view = MockRepository.GenerateStub<ILookupWidgetView>();
  153.             var asyncManager = new TestAsyncTaskManager();
  154.             var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
  155.             var widget = new Widget { Id = 1, Name = "Test" };
  156.  
  157.             widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
  158.                 .ExecuteAsyncCallback().Return(null);
  159.             widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
  160.                 .Return(widget);
  161.  
  162.             var presenter = new LookupWidgetPresenter(view, widgetRepository)
  163.             {
  164.                 AsyncManager = asyncManager
  165.             };
  166.  
  167.             // Act
  168.             view.Raise(v => v.Load += null, view, new EventArgs());
  169.             view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Name = "Test" });
  170.             presenter.ReleaseView();
  171.  
  172.             // Assert
  173.             Assert.AreEqual(true, view.Model.ShowResults);
  174.         }
  175.     }
  176. }

Cum bine vedeți diferența dintre ASP.NET MVC și WebFormsMVP s-a cam diminuat considerabil aplicând șablonul Model-View-Presenter, și lăsând inițializarea presenterului pe seama uneltei IoC.

Pe final

Ce am prezentat este doar aperitiv, și nici măcar atâta. Recunosc că nu am avut timp să folosesc prea mult framework-ul, însă scopul este de a vă stârni curiozitatea și de a domoli puțin argumente de tipul MVC e mai tare decât ASP.NET pentru că ai o separare mai bună a intereselor, poți folosi inversiunea controlului și injectarea dependințelor, și poți testa mai ușor codul. Ei bine nu, și în ASP.NET WebForms se poate, doar că nu a existat o mișcare susținută în această direcție.

Sper să vă fie de folos și vă rog să experimentați cu concepte precum: „Shared Presenters”, „Cross Presenter Messaging” și „Asynchronous Tasks”. Ân general poate nu veți avea șansa să le întrebuințați, sau poate da..

Numai bine, și să ne găsim sănătoși în următorea postare care va fi puțin mai MVC ;)


Posted Wed, Feb 10 2010 7:52 PM by Mihai Lazar
Filed under: ,

Comments

ignatandrei wrote re: WebForms MVP. Ce este? Cum se folosește?
on Wed, Feb 10 2010 11:49 PM

Yikes - code behind testabil ? ???Evenimente pe controale - alt viewstate ?

Oricum, la max 200 de download - eu zic sa nu ne incurcam...

tudor.t wrote re: WebForms MVP. Ce este? Cum se folosește?
on Thu, Feb 11 2010 9:58 AM

Nimeni nu a zis ca nu se putea folosi model-view-presenter si in ASP.NET WebForms - problema e in "disciplina" programatorului - in exemplu de mai sus, nimic nu-l impiedica pe un programator mai lenes sa puna in event handler (Find_Click) nu doar un raise al unui event catre presenter, ci o gramada de cod de business logic...

Nu e vorba doar de lene - cand e presiune in proiect, si trebuie realizat in 2-3 ore un form cu 20 - 30 de controale, fiecare facand postback, tentatia e mare sa se "sara" peste overhead-ul declararii a inca 20 de event-uri, metode On... etc. ...

Asta nu insemana ca nu poate fi folosit, daca exista vointa - la urma urmei si in Prism ('Composite app. guidance for WPF and Silvelight') se foloseste un model oarecum similar..

boldicu wrote re: WebForms MVP. Ce este? Cum se folosește?
on Wed, Feb 24 2010 2:50 PM

O librarie mai matura ar fi WCSF (Web Client Software Factory) msdn.microsoft.com/.../bb264518.aspx . Include si suport pentru MVP pattern si multe alte componente utile.

Deocamdata se integreaza bine in VS 2008 dar am vazut ca se lucreaza si la versiunea pentru VS 2010 www.codeplex.com/webclientguidance. Oricum se poate folosi si pe VS 2010 dar fara guidance package (un pachet care ajuta la creerea paginilor(user controalelor) cu presenter si view.

(c) RONUA 2004-2009
Powered by Community Server (Commercial Edition), by Telligent Systems