Friday, February 27, 2009

SpecificationManager improvements

So yeah, I can't leave well enough alone.

I liked where I was going with my last post, but thought I could make it a bit better.  Why should a view have a "FailedSpecifications" property? It's nice, it's convenient, but really, what does that mean? You'll ALLWAYS be setting some Error message property, right?

So I did this...

 

    7  /// <summary>

    8     /// An interface for Views that have <see cref="Specification{T}"/>

    9     /// </summary>

   10     /// <typeparam name="T"></typeparam>

   11     public interface IValidatedView<T>

   12     {

   13         /// <summary>

   14         /// The Error Messages

   15         /// </summary>

   16         String ErrorMessage { set; }

   17 

   18         /// <summary>

   19         /// Should probably just return GetLocalResourceObject(key) here

   20         /// </summary>

   21         /// <param name="key"></param>

   22         /// <returns></returns>

   23         String GetErrorMessage(string key);

   24     }

 

I WISH that Page.GetLocalResourceObject was public (yeah, I know why it's not.. but it would be cool for this)... That's the idea, you just return GetLocalResourceObject(key) for the method. Maybe I'll play with this later and do a little reflection magic and not require the GetErrorMessage property, but this works for now...

Why the Type param? It'll get used by the SpecificationManager to find the right specs... I might be able to remove it, hadn't thought about it till just now! I'll look into that too.

Anyway, I digress... So now I want Presenter<T> to do all the work of setting ErrorMessage instead of making the implementer of the view do all that silly looping through the FailedSpecs...

Stub stuff for unit test consumption....

    4 namespace MyNamespace

    5 {

    6     public class ITestView { }

    7     public interface ITestValidatedView:IValidatedView<ITestValidatedView>{}

    8     public class FailedTestSpec:Specification<ITestValidatedView>{

    9         public override bool IsSatisfiedBy(ITestValidatedView obj)

   10         {

   11             return false;

   12         }

   13     }

 

Unit Tests....

 

    9  [TestFixture]

   10     public class PresenterTestFixture

   11     {

   12         private MockRepository mocker;

   13         private ITestValidatedView view;

   14         private Presenter<ITestValidatedView> presenter;

   15         private const string failedSpecName = "FailedTestSpec";

   16         private const string errorMessage = "ErrorMessage";

   17 

   18         public void Arrange()

   19         {

   20             mocker = new MockRepository();

   21             view = mocker.DynamicMock<ITestValidatedView>();

   22             SetupResult.For(view.GetErrorMessage(failedSpecName)).Return(errorMessage);

   23         }

   24         public void Act()

   25         {

   26             presenter = mocker.PartialMock<Presenter<ITestValidatedView>>(view);

   27             mocker.ReplayAll();

   28         }

   29         [Test]

   30         public void ViewIsValidSetsErrorMessage()

   31         {

   32             Arrange();

   33             view.ErrorMessage = errorMessage;

   34             Act();

   35             presenter.ViewIsValid();

   36             mocker.VerifyAll();

   37         }

   38         [Test]

   39         public void ViewIsValid_returns_false()

   40         {

   41             Arrange();

   42             Act();

   43             Assert.That(presenter.ViewIsValid(),Is.False);

   44         }

   45     }

 

So all that's happening is that I'm making the concrete implementation if ITestValidatedView's GetErrorMessage always return a string that I can do assertions against. The Presenters ViewIsValid() method should set the ErrorMessage to whatever the view returns for GetErrorMessage for that failed spec. Of course, I've got the unit test that makes sure that ViewIsValid() returns the right value.

 

I really need to right one for a valid view too... I'll get to it, I promise.

 

Here's the code of interest in Presenter

 

   33 /// <summary>

   34         /// Validates the View, also Sets Failed Messages if the View is <see cref="IValidatedView{T}"/>

   35         /// </summary>

   36         /// <returns></returns>

   37         protected internal bool ViewIsValid()

   38         {

   39             return !SetFailedSpecifications().HasContent();

   40         }

   41         /// <summary>

   42         /// Returns the <see cref="Specification{T}"/> that the <see cref="TView"/> does not satisfy.

   43         /// </summary>

   44         /// <returns></returns>

   45         protected Collection<Specification<TView>> SetFailedSpecifications()

   46         {

   47             var failedSpecs = ViewSpecificationManager.GetFailedSpecifications(view);

   48             SetValidatedViewErrorMessage(failedSpecs);

   49             return failedSpecs;

   50         }

   51 

   52         private void SetValidatedViewErrorMessage(IEnumerable<Specification<TView>> failedSpecs)

   53         {

   54             var validatedView = view as IValidatedView<TView>;

   55             if (validatedView == null) return;

   56             var builder = new StringBuilder();

   57             foreach (var spec in failedSpecs)

   58             {

   59                 builder.Append(validatedView.GetErrorMessage(spec.GetType().Name));

   60             }

   61             validatedView.ErrorMessage = builder.ToString();

   62         }

   63     }

 

So, yep, now, from any presenter, you can just call ViewIsValid() and it'll take care of setting error messages for ANY Specification<View> in your assembly.

 

Here's an example of it in use in an actual presenter in some of my code.

 

   45 private void SaveContentItem(object sender, EventArgs e)

   46         {

   47             if (ViewIsValid())

   48             {

   49                 var contentItem = CreateContentItem();

   50                 repository.Save(contentItem);

   51             }

   52         }

 

nice and clean... Reads really nice (at least I think)...

 

Here's the code behind for the page for the view. It's just a really silly simple CMS, this peticular implementation doesn't ever "Edit" it just creates.

 

    5 public partial class Pages_Default :MvpPage,ICreateOrEditContentItemView

    6 {

    7 

    8     public int? ContentItemId

    9     {

   10         get { return null; }

   11 

   12     }

   13 

   14     public string PageTitle

   15     {

   16         get { return TitleControl.Text; }

   17     }

   18 

   19     public string Text

   20     {

   21         get { return BodyControl.Text; }

   22     }

   23 

   24     public string Url

   25     {

   26         get { return UrlControl.Text.Trim(); }

   27     }

   28 

   29     public string ErrorMessages

   30     {

   31         set { ErrorMessagesControl.Text = value; }

   32     }

   33     public event EventHandler SaveContentItemClicked = delegate { };

   34     protected override void OnInit(EventArgs e)

   35     {

   36         base.OnInit(e);

   37         SaveButton.Click += delegate { SaveContentItemClicked(this, e); };

   38     }

   39     public string ErrorMessage

   40     {

   41         set { ErrorMessagesControl.Text = value; }

   42     }

   43 

   44     public string GetErrorMessage(string key)

   45     {

   46         return (string)GetLocalResourceObject(key);

   47     }

   48 }

 

Hope you like it!

No comments:

Post a Comment