Showing posts with label Rails. Show all posts
Showing posts with label Rails. Show all posts

2007-06-23

I've changed my mind on MonoRail

It is amazing!

I'll leave that previous post just in case anyone ever needs to create an ECO object instance first and then connect it with an EcoSpace afterwards, but you don't need it for MonoRail!

  public void Join([EcoDataBind("User")]User user)

{

PropertyBag["User"] = user;

if (this.Params["User.FirstName"] != null)

{

GetErrors(user.AsIObject(), Errors);

if (Context.Params["ConfirmEmailAddress"] != user.EmailAddress)

Errors.Add("Email address confirmation does not match email address.");

if (Errors.Count == 0)

{

//TODO EcoSpace.UpdateDatabase();

Redirect("Account", "Home");

}//No errors

}

}


That's all there is to it now! I have created a small set of classes to enable ECO support in MonoRail. Instead of descending your controllers from SmartDispatcherController you will now descend from EcoSmartDispatcherController. This gives the following abilities:

  1. The EcoSpace instance is created automatically for each request.

  2. If the EcoSpace is dirty at the end of the request it is serialized and stored in the Session. Next request it is deserialized from the Session instead of being created new.

  3. You can use the [EcoDataBinder] attribute as displayed above. This will ensure that the object instance of the parameter type is created within the EcoSpace of the controller. No need to modify your business classes at all. Form data is automatically bound to the object, and when posted the binding is automatically reversed.

  4. You can add the ExternalId to your form as a hidden input like so $EcoFormHelper.ExternalId("User", $User).



I really hope I don't find an insurpassable obstacle with this framework, because I can really see myself liking it!

MonoRails, loving it

So I didn't like Ruby on Rails much. More accurately I didn't like the Ruby language or ActiveRecord much, but the "Rails" part I really quite liked! So my investigation continues and I have found myself looking at MonoRails.

MonoRails is what I would have as a child called "a rip off", but these days it is known as a "clone" :-) It's basically a .NET version of Rails, which obviously appeals to me because I liked the Model-View-Controller approach of Rails and I obviously like C#.

MonoRails has its own version of ActiveRecord (which I shall be avoiding) and an interface into NHibernate too (which I haven't looked at in great depth, but it certainly doesn't look as powerful as ECO). So I have been trying to get MonoRails working with ECO instead. Considering I don't know MonoRails at all I am surprised at how quickly I managed to do what I wanted. Take the following controller method as an example, when the user visits localhost/Account/Join the following method will be executed...

public void Join([DataBind("User")] User user)


MonoRails will automatically create an instance of User (my ECO class) and then automatically populate its contents from the form that was posted. The first problem I had here was that ECO classes have no parameterless constructors. I could have gone down the following route:

public void Join(string salutation, string firstName,
string lastName, string emailAddress, string password)


but that would mean I have to create the User instance myself and then populate its properties from the parameters. The problem with this approach is that I am just too damned lazy :-) So, my first hurdle was to allow ECO classes to be constructed + have their properties set without an EcoSpace. I created a base RootClass that all of my business classes will ultimately descend from, and then I did this...

First I created a simple class that implements IContent and stores the values in a Dictionary.
 internal class TemporaryCache : IContent
{
private Dictionary<int, object> MemberValues = new Dictionary<int, object>();
#region IContent Members
object IContent.get_MemberByIndex(int index)
{
return MemberValues[index];
}

void IContent.set_MemberByIndex(int index, object value)
{
MemberValues[index] = value;
}

#endregion

public void ApplyValues(IContent destination)
{
foreach (int currentKey in MemberValues.Keys)
destination.set_MemberByIndex(currentKey, MemberValues[currentKey]);
}
}


All other methods of IContent throw a NotImplementedException, all we need here is get_MemberByIndex and set_MemberByIndex so that simple property values (not associations) may be set before the object has an EcoSpace. At some point we need to update the values in the EcoSpace, which is exactly what ApplyValues does, it just copies its own values across to a target IContent.

Now we need to use this class as our business class’s IContent and implement a parameterless constructor.

  public RootClass()
{
eco_Content = new TemporaryCache();
}


And finally we need at some point after creation + setting the property values to be able to attach this object + its contents to the new EcoSpace.

  public void AttachToEcoSpace(DefaultEcoSpace ecoSpace)
{
if (ecoSpace == null)
throw new ArgumentNullException("ecoSpace");
if (this.eco_Content != null && !(this.eco_Content is TemporaryCache))
throw new InvalidOperationException("EcoSpace already assigned");

TemporaryCache oldContent = (TemporaryCache)eco_Content;
eco_Content = null;
this.Initialize(ecoSpace);
oldContent.ApplyValues(eco_Content);
}


Now my Join() method looks like this:
  public void Join([DataBind("User")] User user)
{
user.AttachToEcoSpace(EcoSpace);
PropertyBag["User"] = user;
if (this.Params["User.FirstName"] != null)
{
user.GetErrors(Errors);
if (Context.Params["ConfirmEmailAddress"] != user.EmailAddress)
Errors.Add("Email address confirmation does not match email address.");
if (Errors.Count == 0)
{
EcoSpace.UpdateDatabase();
Redirect("Account", "Home");
}//No errors
}
}


Standard stuff


I descend all new Controller classes from my own abstract BaseController which merely creates a new EcoSpace at the start of a request and disposes it at the end. This class also exposes an Errors property (List<string>) that I can add errors to.

 public class BaseController : SmartDispatcherController
{
protected readonly List<string> Errors = new List<string>();

protected override void Initialize()
{
base.Initialize();
EcoSpace = new ApplicationEcoSpace();
PropertyBag["Errors"] = Errors;
}

protected override void ReleaseResources()
{
EcoSpace.Dispose();
base.ReleaseResources();
}

private ApplicationEcoSpace ecoSpace;
protected ApplicationEcoSpace EcoSpace
{
get
{
ecoSpace.Active = true;
return ecoSpace;
}
private set
{
ecoSpace = value;
}
}
}

My Account controller then descends from this, like so:
 [Layout("default")]
[Rescue("generalerror")]
public class AccountController : BaseController
{
public void Join([DataBind("User")] User user)
{
user.AttachToEcoSpace(EcoSpace);
PropertyBag["User"] = user;
if (this.Params["User.FirstName"] != null)
{
user.GetErrors(Errors);
if (Context.Params["ConfirmEmailAddress"] != user.EmailAddress)
Errors.Add("Email address confirmation does not match email address.");
if (Errors.Count == 0)
{
EcoSpace.UpdateDatabase();
Redirect("Account", "Home");
}//No errors
}
}
}


Why do I set PropertyBag["User"] = user;? Simple, I can then databind to it in my view...
<h2>Join</h2>
$HtmlHelper.Form("Join.rails")
<table summary="Enter your information">
<caption>Please provide the following information.</caption>
<tbody>
<tr>
<th>
<label for="User.Salutation">Salutation</label>
</th>
<td>$HtmlHelper.InputText("User.Salutation", $User.Salutation)</td>
</tr>
<tr>
<th><label for="User.FirstName">First name</label></th>
<td>$HtmlHelper.InputText("User.FirstName", $User.FirstName)</td>
</tr>
<tr>
<th>
<label for="User.LastName">Last name</label>
</th>
<td>$HtmlHelper.InputText("User.LastName", $User.LastName)</td>
</tr>
<tr>
<th>
<label for="User.EmailAddress">Email address</label>
</th>
<td>$HtmlHelper.InputText("User.EmailAddress", $User.EmailAddress)</td>
</tr>
<tr>
<th>
<label for="ConfirmEmailAddress">Confirm email address</label>
</th>
<td>$HtmlHelper.InputText("ConfirmEmailAddress", $ConfirmEmailAddress)</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<input type="Submit" value="Join"/>
</td>
</tr>
</tfoot>
</table>
$HtmlHelper.EndForm

Did you notice the [Layout("default")] on my AccountController? That tells MonoRails to use my default.vm file as the master page for the HTML output. It basically sets up the header information etc and displays any errors, here is how it looks:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<meta name="keywords" content="" />
<title>NotASausage</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<link href="/StyleSheets/Main.css" rel="stylesheet" media="all" type="text/css" />
<link href="/StyleSheets/Menu.css" rel="stylesheet" media="all" type="text/css" />

<link href="/StyleSheets/ThumbnailViewer.css" rel="stylesheet" media="all" type="text/css" />
<script src="/Scripts/ThumbnailViewer.js" type="text/javascript"></script>
</head>
<body>
<div id="MainFrame">
<div id="Header"><img id="HeaderBanner" src="/Images/Banner.jpg" alt="NotASausage" /></div>
<div id="SideBar">
<div id="MainMenu">
<h2>Menu</h2>
<ul>
<li><a href="/default.aspx">Main page</a></li>
</ul>
</div>
</div>
<div id="Contents">
#if ($Errors && $Errors.Count > 0)
<ul class="ErrorList">
#foreach($ErrorMessage in $Errors)
<li>$ErrorMessage</li>
#end
</ul>
#end
$childContent
<div id="Footer">
©Peter Morris - All rights reserved.
</div>
</div>
</body>
</html>

The $childContent indicates where to inject the output of the current page.

How cool is that? I can now model in ECO, generate my business objects, automatically generate my DB, and then finally put together some very simple application actions which will control access to those business objects and display any validation problems back to the user!

So far I like what I see!