Î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
- protected void Application_Start(object sender, EventArgs e)
- {
- PresenterBinder.Factory = new UnityPresenterFactory(build=>
- {
- build.RegisterType<IAmInjected, AmInjected>(
- new Microsoft.Practices.Unity.ContainerControlledLifetimeManager());
- build.RegisterType<HelloWorldPresenter>(
- new TransientLifetimeManager());
- });
- }
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
- internal class ScriptsServicesBuilder
- {
-
- public static void Build(IUnityContainer container)
- {
-
- // register javascript cacher
- container
- .RegisterType<IScriptCompressionService, MicrosoftAjaxMinifierCompressionService>()
- .RegisterType<IScriptsResolverService, ScriptsConfigurationSectionResolverService>(JS_CONFIGURATION_RESOLVER_SERVICE,
- new ContainerControlledLifetimeManager(),
- new InjectionConstructor(
- new ResolvedParameter<IScriptCompressionService>()
- )
- )
- .RegisterType<IScriptsResolverService, CachedScriptsResolverService>(
- new ContainerControlledLifetimeManager(),
- new InjectionConstructor(
- new ResolvedParameter<IScriptsResolverService>(JS_CONFIGURATION_RESOLVER_SERVICE),
- new ResolvedParameter<ICacheManager>(SCRIPTS_CACHE_MANAGER),
- lExpiration
- )
- );
- }
- }
Iar în builder putem configura ceva în genul
Code Snippet
- PresenterBinder.Factory = new UnityPresenterFactory(build=>
- {
- ScriptsServicesBuilder.Build(build);
- build.RegisterType<IAmInjected, AmInjected>(
- new Microsoft.Practices.Unity.ContainerControlledLifetimeManager());
- build.RegisterType<HelloWorldPresenter>(
- new TransientLifetimeManager());
- });
Definirea unui Presenter
Code Snippet
- public class HelloWorldPresenter
- : Presenter<IView<HelloWorldModel>>
- {
- readonly IAmInjected amInjected;
-
- public HelloWorldPresenter(IView<HelloWorldModel> view, IAmInjected amInjected)
- : base(view)
- {
- this.amInjected = amInjected;
- View.Load += View_Load;
- }
-
- public override void ReleaseView()
- {
- View.Load -= View_Load;
- }
-
- void View_Load(object sender, EventArgs e)
- {
- SetMessage();
- }
-
- private void SetMessage()
- {
- View.Model.Message = HttpContext.User.Identity.IsAuthenticated
- ? String.Format("Hello {0}!", HttpContext.User.Identity.Name)
- : "Hello World!";
- }
- }
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
- <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="HelloWorldControl.ascx.cs" Inherits="WebFormsMvp.FeatureDemos.Web.Controls.HelloWorldControl" %>
- <div class="hello-world">
- <%# Model.Message %>
- </div>
Iar clasa
Code Snippet
- using System;
- using WebFormsMvp.FeatureDemos.Logic.Presenters;
- using WebFormsMvp.FeatureDemos.Logic.Views.Models;
- using WebFormsMvp.Web;
-
- namespace WebFormsMvp.FeatureDemos.Web.Controls
- {
- [PresenterBinding(typeof(HelloWorldPresenter))]
- public partial class HelloWorldControl : MvpUserControl<HelloWorldModel>
- {
- }
- }
Foarte similar cu modelul din ASP.NET MVC, diferența majoră o vom observa în view-uri mai complexe precum
Code Snippet
- <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LookupWidgetControl.ascx.cs" Inherits="WebFormsMvp.FeatureDemos.Web.Controls.LookupWidgetControl" %>
- <div class="lookup-widget">
- <fieldset><legend>Enter ID or Name of widget</legend>
- <ol>
- <li>
- <asp:Label runat="server" AssociatedControlID="widgetId">ID: </asp:Label>
- <asp:TextBox runat="server" ID="widgetId" MaxLength="9" />
- <div class="validators">
- <asp:CompareValidator runat="server" ControlToValidate="widgetId"
- ValidationGroup="LookupWidget"
- Display="Dynamic" Type="Integer" Operator="DataTypeCheck"
- ErrorMessage="ID must be a valid whole number" />
- <asp:RangeValidator runat="server" ControlToValidate="widgetId"
- ValidationGroup="LookupWidget"
- Display="Dynamic" Type="Integer" MinimumValue="1" MaximumValue="9999999"
- ErrorMessage="ID must be a positive whole number" />
- </div>
- </li>
- <li>
- <asp:Label runat="server" AssociatedControlID="widgetName">Name: </asp:Label>
- <asp:TextBox runat="server" ID="widgetName" MaxLength="255" />
- </li>
- </ol>
- <p>
- <asp:Button runat="server" Text="Find" ValidationGroup="LookupWidget"
- OnClick="Find_Click" />
- </p>
- </fieldset>
- <div class="results">
- <asp:DetailsView ID="results" runat="server" DataSource="<%# Model.Widgets %>"
- EmptyDataText="No matching results found"
- Visible="<%# Model.ShowResults %>" />
- </div>
- </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
-
- namespace WebFormsMvp.FeatureDemos.Web.Controls
- {
- public partial class LookupWidgetControl
- : MvpUserControl<LookupWidgetModel>, ILookupWidgetView
- {
- protected void Find_Click(object sender, EventArgs e)
- {
- int? id = String.IsNullOrEmpty(widgetId.Text) ?
- null : id = Convert.ToInt32(widgetId.Text);
- OnFinding(id, widgetName.Text);
- }
-
- public event EventHandler<FindingWidgetEventArgs> Finding;
- private void OnFinding(int? id, string name)
- {
- if (Finding != null)
- {
- Finding(this, new FindingWidgetEventArgs() { Id = id, Name = name });
- }
- }
- }
- }
Logica din presenter este:
Code Snippet
- using System;
- using System.Collections.Generic;
- using WebFormsMvp.FeatureDemos.Logic.Data;
- using WebFormsMvp.FeatureDemos.Logic.Views;
-
- namespace WebFormsMvp.FeatureDemos.Logic.Presenters
- {
- public class LookupWidgetPresenter
- : Presenter<ILookupWidgetView>
- {
- private readonly IWidgetRepository widgetRepository;
-
- public LookupWidgetPresenter(ILookupWidgetView view)
- : this(view, null)
- { }
-
- public LookupWidgetPresenter(ILookupWidgetView view, IWidgetRepository widgetRepository)
- : base(view)
- {
- this.widgetRepository = widgetRepository ?? new WidgetRepository();
- View.Finding += View_Finding;
- View.Model.Widgets = new List<Widget>();
- }
-
- public override void ReleaseView()
- {
- View.Finding -= View_Finding;
- }
-
- void View_Finding(object sender, FindingWidgetEventArgs e)
- {
- if ((!e.Id.HasValue || e.Id <= 0) && String.IsNullOrEmpty(e.Name))
- return;
-
- if (e.Id.HasValue && e.Id > 0)
- {
- AsyncManager.RegisterAsyncTask(
- (asyncSender, ea, callback, state) => // Begin
- {
- return widgetRepository.BeginFind(e.Id.Value, callback, state);
- },
- result => // End
- {
- var widget = widgetRepository.EndFind(result);
- if (widget != null)
- {
- View.Model.Widgets.Add(widget);
- }
- },
- result => { } // Timeout
- , null, false);
- }
- else
- {
- AsyncManager.RegisterAsyncTask(
- (asyncSender, ea, callback, state) => // Begin
- {
- return widgetRepository.BeginFindByName(e.Name, callback, state);
- },
- result => // End
- {
- var widget = widgetRepository.EndFindByName(result);
- if (widget != null)
- {
- View.Model.Widgets.Add(widget);
- }
- },
- result => { } // Timeout
- , null, false);
- }
- AsyncManager.ExecuteRegisteredAsyncTasks();
- View.Model.ShowResults = true;
- }
- }
- }
Testarea presenter-ului.
Code Snippet
- using System;
- using System.Linq;
- using Microsoft.VisualStudio.TestTools.UnitTesting;
- using WebFormsMvp.FeatureDemos.Logic.Presenters;
- using Rhino.Mocks;
- using WebFormsMvp.FeatureDemos.Logic.Views;
- using WebFormsMvp.FeatureDemos.Logic.Data;
- using WebFormsMvp.Testing;
-
- namespace WebFormsMvp.FeatureDemos.UnitTests
- {
- [TestClass]
- public class LookupWidgetPresenterTests
- {
- [TestMethod]
- public void LookupWidgetPresenterLoadsWidgetFromId()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var asyncManager = new TestAsyncTaskManager();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
- var widget = new Widget {Id = 1, Name = "Test"};
-
- widgetRepository.Stub(w => w.BeginFind(1, null, null)).IgnoreArguments()
- .ExecuteAsyncCallback().Return(null);
- widgetRepository.Stub(w => w.EndFind(null)).IgnoreArguments()
- .Return(widget);
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository)
- {
- AsyncManager = asyncManager
- };
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = 1 });
- asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(widget, view.Model.Widgets.First());
- }
-
- [TestMethod]
- public void LookupWidgetPresenterLoadsWidgetFromIdWhenBothIdAndNameSet()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var asyncManager = new TestAsyncTaskManager();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
- var widget = new Widget { Id = 1, Name = "Test" };
-
- widgetRepository.Stub(w => w.BeginFind(1, null, null)).IgnoreArguments()
- .ExecuteAsyncCallback().Return(null);
- widgetRepository.Stub(w => w.EndFind(null)).IgnoreArguments()
- .Return(widget);
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository)
- {
- AsyncManager = asyncManager
- };
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = 1, Name = "Blah" });
- asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(widget, view.Model.Widgets.First());
- }
-
- [TestMethod]
- public void LookupWidgetPresenterLoadsWidgetFromName()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var asyncManager = new TestAsyncTaskManager();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
- var widget = new Widget {Id = 1, Name = "Test"};
-
- widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
- .ExecuteAsyncCallback().Return(null);
- widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
- .Return(widget);
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository)
- {
- AsyncManager = asyncManager
- };
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Name = "Test" });
- asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(widget, view.Model.Widgets.First());
- }
-
- [TestMethod]
- public void LookupWidgetPresenterLoadsWidgetFromNameWhenBothIdAndNameSetButIdIsInvalid()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var asyncManager = new TestAsyncTaskManager();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
- var widget = new Widget { Id = 1, Name = "Test" };
-
- widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
- .ExecuteAsyncCallback().Return(null);
- widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
- .Return(widget);
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository)
- {
- AsyncManager = asyncManager
- };
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Id = -1, Name = "Test" });
- asyncManager.ExecuteRegisteredAsyncTasks(); // Execute the tasks here as ASP.NET would normally do for us
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(widget, view.Model.Widgets.First());
- }
-
- [TestMethod]
- public void LookupWidgetPresenterHidesResultsOnInitialLoad()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository);
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(false, view.Model.ShowResults);
- }
-
- [TestMethod]
- public void LookupWidgetPresenterShowsResultsOnFinding()
- {
- // Arrange
- var view = MockRepository.GenerateStub<ILookupWidgetView>();
- var asyncManager = new TestAsyncTaskManager();
- var widgetRepository = MockRepository.GenerateStub<IWidgetRepository>();
- var widget = new Widget { Id = 1, Name = "Test" };
-
- widgetRepository.Stub(w => w.BeginFindByName("Test", null, null)).IgnoreArguments()
- .ExecuteAsyncCallback().Return(null);
- widgetRepository.Stub(w => w.EndFindByName(null)).IgnoreArguments()
- .Return(widget);
-
- var presenter = new LookupWidgetPresenter(view, widgetRepository)
- {
- AsyncManager = asyncManager
- };
-
- // Act
- view.Raise(v => v.Load += null, view, new EventArgs());
- view.Raise(v => v.Finding += null, view, new FindingWidgetEventArgs { Name = "Test" });
- presenter.ReleaseView();
-
- // Assert
- Assert.AreEqual(true, view.Model.ShowResults);
- }
- }
- }
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