Showing posts with label MonoRail. Show all posts
Showing posts with label MonoRail. Show all posts

2008-02-13

Unit testing MonoRail controllers

I spent yesterday finishing off (mostly) my business model, then the end of yesterday + today writing test cases for those classes. Everything was going great, I found at least 3 errors in my code that I hadn’t realised was there and also realised there were a few more things I needed.

Then it was time to start testing the controllers in my MonoRail site. What a disaster!

Attempt 1:
[Test]
public void AdminOnly_Home()
{
  AdminController controller = new AdminController();
  controller.Home();
  Assert.IsTrue(Controller.Response.WasRedirected, "Should have been redirected");
}


The problem with this was pretty obvious, Controller doesn’t have a Response etc set up. So along came attempt 2:

[Test]
public void AdminOnly_Home()
{
  AdminController controller = new AdminController();
  PrepareController(controller);
  controller.Home();
  Assert.IsTrue(Controller.Response.WasRedirected, "Should have been redirected");
}


Now the controller is set up with mock objects and will run! Unfortunately the BeforeAction filter on my action was not being executed. Aha! Pretty obvious problem! If I call the method directly how can the framework possible find all of the reflection attributes and process them etc? *slaps head*

Attempt 3
[Test]
public void AdminOnly_Home()
{
  AdminController controller = new AdminController();
  PrepareController(controller, "Admin", "Home");
  controller.Process(Controller.Context, Controller.ControllerContext);
  Assert.IsTrue(Controller.Response.WasRedirected, "Should have been redirected");
}


Still no joy! The filters just aren’t being executed. Someone on the user groups said that this is expected behaviour and that the filter should be tested in isolation. Whereas I agree for the most part unfortunately it doesn’t apply in this case. My ECO extensions to MonoRail allow the developer to specify pooling, session, default EcoSpace types, and so on. If these reflection attributes aren’t processed then the action just isn’t going to act in the same way it will at runtime!

At the moment I am sorely disappointed! I was really looking forward to writing a test driven website but unless this guy was wrong it doesn’t look like it is going to be possible!

It’s at times like these I wonder how difficult it really is to write your own MVC framework? Maybe I will take another look at the MS offering. If I had enough free time I'd make my own :-)

2008-02-09

Validation

I have a model like so

Product 1----* ProductVersion
ProductVersion 1----* ProductEdition

ProductVersion can been in one of two states: UnderDevelopment / Released

ProductEdition has a DownloadUrl:string attribute which is only required if self.version.status = #Released


The validation for ProductEdition works perfectly, I cannot leave the DownloadUrl blank if the ProductVersion has already been released. Unfortunately when I already have a number of ProductEdition

instances with no DownloadUrl and then make my Productversion live the editions are not validated because they are not dirty. So I needed some way to ensure that when ProductVersion is validated all related

ProductEdition instances are also validated.

Step 01: Add a way to allow ProductVersion to identify other objects to be validated.

In the business classes project I added the following interface.

public interface IValidationExtender
{
  IEnumerable GetConstraintedObjects();
}


My ProductVersion can do this

IEnumerable IValidationExtender.GetConstraintedObjects()
{
  List result = new List();
  foreach (IObject currentEdition in Editions)
    result.Add(currentEdition.AsIObject());
  return result;
}



Step 02: Create a validation service which validates all objects : Only implemented methods are shown

public class ExtendedConstraintProvider : IConstraintProvider
{
 private IConstraintProvider ModeledConstraintProvider;

 public void GetConstraintsForObject(IObject instance, List constraints)
 {
    if (instance == null)
      throw new ArgumentNullException("instance");

    //Deletegate to GetConstraintsForObjects
    GetConstraintsForObjects((IObjectList)instance.GetAsCollection(), constraints);
  }

  public void GetConstraintsForObjects(IObjectList objectList, List constraints)
  {
    if (objectList == null)
      throw new ArgumentNullException("objectList");
    if (objectList.Count == 0)
      return;

    //Get all constrained objects
    Dictionary includedObjects = new Dictionary();
    foreach (IObject currentObject in objectList)
      RecursiveGetExtendedObjects(currentObject, includedObjects);

    //Add the objects to a list
    IObjectList newInstances = EcoServiceHelper.GetVariableFactoryService(objectList[0]).CreateUntypedObjectList(true);
    foreach (KeyValuePair kvp in includedObjects)
      newInstances.Add(kvp.Key);

    //Return the constraints from ModeledConstraintProvider
    ModeledConstraintProvider.GetConstraintsForObjects(newInstances, constraints);
  }

  private void RecursiveGetExtendedObjects(IObject currentObject, Dictionary includedObjects)
  {
    //Don't process the same object twice
    if (includedObjects.ContainsKey(currentObject))
      return;

    includedObjects.Add(currentObject, null);

    //If the class implements IValidationExtender then add its constrained objects
    IValidationExtender extender = currentObject.AsObject as IValidationExtender;
    if (extender != null)
    {
      foreach (IObject dependentObject in extender.GetConstraintedObjects())
        RecursiveGetExtendedObjects(dependentObject, includedObjects);
    }
  }
}




Step 03: Register the service in the EcoSpace

public InteevoWebsiteEcoSpace(): base()
{
  InitializeComponent();
  RegisterEcoService(typeof(IConstraintProvider), new ExtendedConstraintProvider());
}



Now I can validate a list of dirty objects using EcoSpace.GetEcoService().GetConstraintsForObjects.


Step 04: Last point of defence, ensure that no invalid objects may be saved. Only relevant methods are shown.

internal class ValidatingPersistenceService : IPersistenceService
{
  private IPersistenceService Inner;
  private IEcoServiceProvider ServiceProvider;

  internal ValidatingPersistenceService(IEcoServiceProvider serviceProvider)
  {
    if (serviceProvider == null)
      throw new ArgumentNullException("serviceProvider");

    ServiceProvider = serviceProvider;
    Inner = ServiceProvider.GetEcoService();
    if (Inner == null)
      throw new ArgumentException("ServiceProvider did not provide an instance for IPersistenceService");
  }

  private IConstraintProvider constraintProvider;
  private IConstraintProvider ConstraintProvider
  {
    get
    {
      if (constraintProvider == null)
      {
        constraintProvider = ServiceProvider.GetEcoService();
        if (constraintProvider == null)
          throw new InvalidOperationException("IConstraintProvider not registered as an ECO service");
      }
      return constraintProvider;
    }
  }



  void IPersistenceService.UpdateDatabaseWithList(IObjectList list)
  {
    ValidateObjects(list);
    Inner.UpdateDatabaseWithList(list);
  }

  private void ValidateObjects(IObjectList objects)
  {
    List constraints = new List();
    ConstraintProvider.GetConstraintsForObjects(objects, constraints);
    foreach (DroopyEyes.EcoExtensions.Validation.IConstraint currentConstraint in constraints)
    {
      if (!currentConstraint.IsValid)
      {
        throw new InvalidOperationException(
          string.Format("Cannot update database with invalid objects:\r\n{0} : {1}",
            currentConstraint.Instance.UmlClass.Name, currentConstraint.Name)
        );
      }
    }//foreach constraint
  }
}



Step 05: Replace the standard IPersistenceService in the EcoSpace

public InteevoWebsiteEcoSpace(): base()
{
  InitializeComponent();
  RegisterEcoService(typeof(IPersistenceService), new ValidatingPersistenceService(this));
  RegisterEcoService(typeof(IConstraintProvider), new ExtendedConstraintProvider());
}



Finally I have an implementation which does the following

A: Allows me to get constraints for dirty objects + all relevant objects
B: Prevents the app from saving objects with broken constraints.

2008-02-07

EcoRail

The whole idea of having a controller and a view is so that the view renders only exactly what it is given, and the controller is able to give it whatever data it likes from wherever it needs to obtain it.

After working with ECO and Monorail for a while it has been a real pleasure, but I am starting to think that maybe exposing ECO objects directly to the view is not the right approach.

If for example I put an Employee into the PropertyBag the view can easily display $Employee.Salary. This might not be a problem when you develop both the controllers and the view but in my case someone else will ultimately create the views. Do I really want them to be able to have access to this information? In addition, what if the view engine they use has a scripting language that is able to set values? Setting $Employee will merely set the PropertyBag["Employee"] value, but setting $Employee.Salary could see a certain view developer buying a new car next month.

I am very tempted to change the site whilst it is in its early stages of development. It does seem more logical to have small chunks of data or small classes to pass back and forth between the controller and the view. This is more in line with the design I have in my PocketPC application.

If that is the case it will probably mean that EcoRails is redundant! Actually, only the EcoDataBind part would really be redundant I think, the rest would still be quite useful!

2008-02-05

MaxLength

Implementing HTML maxlength was a bit of a pain. Not to write the helpers though, that was easy....

$EcoModelHelper.AttributeLength($Product, "ID")


But when it came to specifying that in the <input> it was too much work! This is how it is done statically...

$FormHelper.TextFieldValue("Product.ID", $Product.ID, "%{maxlength='32'}")


Now I had to replace the static 32 with the EcoModelHelper code.

#set ($ProductIDLength = $EcoModelHelper.AttributeLength($Product, "ID"))
$FormHelper.TextFieldValue("Product.ID", $Product.ID, "%{maxlength='$ProductIDLength'}")


This was starting to look like too much typing!

So instead I have decided to add new methods to the EcoFormHelper. Here is the first:

$EcoFormHelper.ObjectTextField("Product.ID", $Product, "ID")


This will output something like this

<input type="text" id="Product_ID" name="Product.ID" value="AlterEgo" maxlength="32" />

It just uses the normal MonoRail $FormHelper.TextFieldValue helper but passes it the current value of the object and the maximum length as defined in the model

More work up front, less in the long run :-)

EcoRail validation

Here is yesterday's update.

I wanted a way to validate the user input. Seeing as there are constraints in the model to me this was the obvious approach to take. The HTML in my main layout (MasterPage) was changed like so

<body>
  #if ($Errors && $Errors.Count > 0)
    <ul class="errors">
      #foreach ($currentError in $Errors)
        <li>$currentError</li>
      #end
    </ul>
  #end

  $childContent

</body>


This outputs all errors passed in PropertyBag["Errors"] or in my case I used Flash["Errors"].


To validate my product input I changed my controller like so:

[AllowEcoSpaceDeactivateDirty(true)]
public void Modify([EcoDataBind("Product", Allow = "ID,Name", NoObjectIdAction = ObjectIdAction.CreateNewInstance)]Product product)
{
  PropertyBag["Product"] = product;
  IList<string> errors = GetErrorsForAllDirtyObjects();
  if (errors.Count > 0)
    Flash["Errors"] = errors;
  else
  {
    EcoSpace.UpdateDatabase();
    RedirectToAction("List");
  }
}


GetErrorsForAllDirtyObjects uses the DefaultEcoSpaceType to find the EcoSpace instance and then checks all constraints of all dirty objects in order to return a list of strings. Available validation routines are


protected IList<string> GetErrorsForObject(IObjectProvider instance)

Gets error messages for broken constraints on a single object


protected IList<string> GetErrorsForAllDirtyObjects(Type ecoSpaceType)

Gets the EcoSpace instance of the type specified and then returns errors messages for broken constraints on all modified objects


protected IList<string> GetErrorsForAllDirtyObjects()

Calls GetErrorsForAllDirtyObjects(Type ecoSpaceType) using the DefaultEcoSpaceType specified



Now I have to take into account that not everyone wants to have their error messages returned from OCL constraints defined in the model. To cater for this my validation routines do not directly read the model, instead they use a virtual property

private IConstraintProvider m_ConstraintProvider;
protected virtual IConstraintProvider ConstraintProvider
{
  get
  {
    if (m_ConstraintProvider == null)
      m_ConstraintProvider = new ModeledConstraintProvider();
    return m_ConstraintProvider;
  }
}


The default implementation returns an instance of ModeledConstraintProvider which is a class in the DroopyEyes.Eco.Extensions project, but you can now override this property on your controller and return any implementation you like.

So now I have OCL validation from the model. Next I think I will add an EcoModelHelper so that you can obtain information from the model, to start with I think all I will implement is something like the following

$EcoModelHelper.Length("Person", "FirstName")

2008-02-03

Changing the URL structure

I wanted the following URL structure in my website

www.mysite.com/product/myproductname/whatsnew

but the default url mapping in MonoRail would translate this as

www.mysite.com/[controller]/[action]/[id]

So it would expect to find this

public class ProductController
{
  public void MyProductName(string id)
  {
  }
}

whereas what I actually want is

public class ProductController
{
  public void WhatsNew(string productName)
  {
  }
}



  1. Open Web.Config

  2. Locate the monorail node

  3. Locate the routing child node

  4. Now add a new <rule> to the top of the list:


<rule>
<pattern>/(product)/(\w+)/(\w+)</pattern>
<replace><![CDATA[ /product/$3.rails?productName=$2]]></replace>
</rule>


As this is at the top of the list it will have the highest priority. If the URL matches /product it will remap the url

From:
www.mysite.com/product/myproductname/whatsnew

To:
www.mysite.com/product/whatsnew.rails?productname=MyProductName


without the user ever seeing it :-)

2008-01-30

MonoRail

I'm working on a new website for work. I've decided to use ECO for the business model due to how much time it saves me. I took a look at the new MVC ASP approach provided by Microsoft recently and was a bit disappointed. There were bugs in some pretty basic errors that would have been an annoyance to code around, and it just didn't feel "ready".

So, I've decided to take another look at MonoRail. I'd already written an ECO implementation for MR in the past but I decided to start the implementation from scratch. This was mainly inspired by the new EcoSpaceManager in ECOIV for ASP .NET. Using an EcoSpaceManager you can easily utilise many instances of different types of EcoSpace in the same page. I decided I would do the same.

Unlike the EcoSpaceManager I haven't gone for unique string values for identifying the EcoSpace instance I want. That approach is good in ASP .NET where you want to bind different components together to generate your HTML but it doesn't really make as much sense when everything you produce is written in code. If you want two instances of the same EcoSpace you can just use EcoSpaceStrategyHandler.GetEcoSpace().

Anyway, on to the detail:

The first thing I have done is to specify EcoSpace and EcoSpaceProvider settings as reflection attributes on the class (controller) and method (action). Like so

[UseEcoSpacePool(true)]
public class AccountController: EcoSmartDispatcherController
{
  public void Index()
  {
  }

  [UseEcoSpacePool(false)]
  public void SomethingElse()
  {
  }
}
In this example the EcoSpace pool will be used for all actions (methods) except SomethingElse which explicitly says not to use it.

If you want to specify EcoSpace type specific settings in your web app then this is possible too:

[UseEcoSpacePool(true)]
[UseEcoSpacePool(typeof(MyEcoSpace), false)]
public class AccountController: EcoSmartDispatcherController
{
  public void Index()
  {
  }

  [UseEcoSpacePool(typeof(MyEcoSpace), true)]
  public void SomethingElse()
  {
  }
}
In this example the default is to use the EcoSpace pool unless you are retrieving an instance of MyEcoSpace in which case it wont be used. However, if the action being invoked is SomethingElse() then retrieving an instance of MyEcoSpace will use the pool. The order of priority is as follows:
  1. Apply settings on the class that are not specific to an EcoSpace type.
  2. Apply settings on the method that are not specific to an EcoSpace type.
  3. Apply settings on the class that are specific to an EcoSpace type.
  4. Apply settings on the method that are specific to an EcoSpace type.
So the order of priority is that Method settings override Class settings, and then EcoSpace type specific settings override non specific settings.

Pretty versatile eh? But how do you get an instance of an EcoSpace? Well, that's pretty simple too!
public void Index()
{
  MyEcoSpace myEcoSpace = GetEcoSpace<MyEcoSpace>();
  //This will return the same instance
  MyEcoSpace myEcoSpace2 = GetEcoSpace<MyEcoSpace>();
}
Any EcoSpaces requested in this manner will automatically be "released" immediately after the method finishes executing so you don't need to worry about it at all. I say "released" because it will either be Disposed, stuffed into the session, or returned to the pool based on your EcoSpaceProvider settings for this class/method/EcoSpace type.

That's not the end of the story though. You can bind instances of your objects directly to HTML and back. To do this you need to identify your business class with a reflection attribute "EcoDataBind", like so
[AllowDeactivateDirty(true)]
public void Create(
  [EcoDataBind(typeof(MyEcoSpace), "Product", CreateIfNoObjectId=true)]Product product)
{
  PropertyBag["Product"] = product;
}
Here I have stated that it is okay to Dispose the EcoSpace instance if it contains dirty objects at the end of this method. The method itself consists of a single parameter named "product" of type "Product". The EcoDataBind might look a bit overwhelming but it says this
  1. The EcoSpace type you need to home the Customer object is MyEcoSpace.
  2. The prefix in the form to look for is "Product", so when you see <input name="Product.Name">> MonoRail knows that the value should go into the Name property of this product.
  3. If there is no ExternalId in the form identifying the object to fetch from the data storage before updating then a new instance should be created.
As a result when you run your app and go to localhost/account/create you will see that the product method has a new instance in there. When the user posts the form back you will again see an instance but containing the updated values. What does the HTML look like? I have used the Brail view engine and HTML helpers to output the HTMl I need. This allows you to use whatever HTML you like but then easily add the <input> etc based on your current object.

${HtmlHelper.Form('create.rails')}

  ${EcoFormHelper.PersistedExternalId('Product.ExternalId', Product)}

  Name : ${HtmlHelper.InputText('Product.Name', Product.Name)}

  Current version number : ${HtmlHelper.InputText('Product.CurrentVersionNumber', Product.CurrentVersionNumber)}

  ${HtmlHelper.SubmitButton('Save')}

${HtmlHelper.EndForm()}
I have split the lines up a bit to make them easier to visually separate.

First an EcoFormHelper is told to output a hidden input named Product.ExternalId for the product we set in the C# method (see PropertyBag["Product"] = product). EcoFormHelper.ExternalId will output the ExternalId for the object, PersistedExternalId will only output if the object is not new, this is useful in situations like this when the object was disposed of with the EcoSpace it belong too and we can just create a new instance.

Next the HtmlHelper gives us an <input> named "Product.Name" and its value is set to whatever is in the Product's Name property. The same is done for CurrentVersion.

A Submit button is then generated so that the user may post their changes.

Summary
Well, this little example shows that I can implement a nice clean MVC style approach to writing web apps with ECO and not have to worry about constructing EcoSpace instances in code, fetching objects from the data storage and so on manually; everything is done for me.