Castle Demo AppCRUD Operations on Projects
I thought about leaving the CRUD for Projects for the readers, but when I started to implement it myself, I realized that there were several things here that I hand't explained. As a quick note, I made some modifications to the code, to structure it better, there shouldn't be any changes to functionality, but I added a few utility functions and made some of the views more generalized.
In general, those changes are from having NoSuchUser and NoSuchProject views to having a single NoSuchObject view, nothing more. Okay, let's talk about projects. A project is a little bit more complicated than a user, since it actually contains a reference to a user. This means that we can't just use exact same techniques as we used for the CRUD operations on the User class. The changes are fairly minors, but they deserve their own post, so here it is, Views\Admin\EditProject.boo:
<?brail
name = ""
defaultAssigneeId = -1
id = "0"
if ?project:
name = project.Name
defaultAssigneeId = project.DefaultAssignee.Id
id = project.Id.ToString()
end
?>
${HtmlHelper.Form('SaveProject.rails')}
${HtmlHelper.FieldSet('Project Details:')}
${HtmlHelper.InputHidden('project.id', id)}
<table>
<tr>
<td>
${HtmlHelper.LabelFor('project.Name','Project name:')}
</td>
<td>
${HtmlHelper.InputText('project.Name',name, 50, 50)}
</td>
<tr>
<tr>
<td>
${HtmlHelper.LabelFor('project.DefaultAssignee.Id','Default Assignee:')}
</td>
<td>
${HtmlHelper.Select('project.DefaultAssignee.Id')}
${HtmlHelper.CreateOptions(allUsers, 'Name','Id', defaultAssigneeId)}}
${HtmlHelper.EndSelect()}
</td>
</tr>
</table>
${HtmlHelper.SubmitButton('Save')}
${HtmlHelper.EndFieldSet()}
${HtmlHelper.EndForm()}
Most of what you see here we have already seen. The only new thing is the Select in the end, which would display the list of all the users that who can be the default assignee to this project. A couple of words on the CreateOptions() method. The first parameter is the collection of items to use, the second is the display text, the third is the value text. The forth is the id of the selected object. As always, this view is going to be used for both editing a project and adding a new one.
Another thing to note is the bolded line at the start of the view, where we use the '?' prefix to denote an optional variable. Where before we have to use IsDefined(), we can now just use the '?' prefix to get a null result when the value has not been defined for the view.
Now we can turn to write the SaveProject() method, which is a bit more complicated:
public void SaveProject([ARDataBind("project", AutoLoadUnlessKeyIs = 0)]Project project,
[ARFetch("project.DefaultAssignee.Id", Create=false, Required=true)] User defaultAssigneee)
{
RenderMessage( "Back to Projects", "Projects");
try
{
if (project.DefaultAssignee == null)
project.DefaultAssignee = defaultAssigneee;
project.Save();
Flash["good"] = string.Format(
"Project {0} was saved successfully.", project.Name);
}
catch (Exception e)
{
Flash["DataBindErrors"] = project.ValidationErrorMessages;
Flash["bad"] = e.Message;
Flash["Debug"] = e.ToString();
}
}
As you can see, I'm using the ARDataBind for the project again, but now I combine it with ARFetch for the defaultAssignee. The reason for that is that while ARDataBind is smart enough to give me the correctly configured object when the object already exists in the database, it's not smart enough to figure out the correct connections by itself, hence, the ARFetch attribute. It simply gives me back the object with the same id as the one that was passed, if I see that the Default Assignee property on the project is null (which will only happen when we create a new project), I set the defaultAssignee manually. ARFetch will simply load a value from the database based on its primary key, so it's not as smart as ARDataBind, but as you can see, it certainly has it uses.
Here is the EditProject() and CreateProject() methods:
public void CreateProject()
{
AddAllUsersToPropertyBag();
RenderView("EditProject");
}
public void EditProject([ARFetch("id")]Project project)
{
AddAllUsersToPropertyBag();
ValidateObjectToSendToView("project", "Projects", project);
}
There isn't much going with them, CreateProject simply push all the users for the view to use, and render the EditProject view. EditProject uses ARFetch to get the correct project and make it (and all the users) available for the view to use. Here are the confirmProjectDelete and DeleteProject methods:
public void ConfirmDeleteProject([ARFetch("id")]Project project)
{
if (project == null)
RenderNoSuchObject("project");
RenderDelete("deleteProject", "Projects", project);
}
public void DeleteProject([ARFetch("id")]Project project)
{
if (project == null)
{
RenderNoSuchObject("project");
return;
}
project.Delete();
RedirectToAction("Projects");
}
You can probably guess that I'm very much in love with the ARFetch attribute :-) I trust that there isn't much need to explain what the code does, it's pretty straight forward. Another change that I've made was with the Global.asax, where the code moved to a code behind file. I also added a session per request support, which will be helpful when we'll start dealing with lazy collections:
public class MythicalBugTrackerApplication : HttpApplication
{
const string SessionScope = "Active.Record.SessionScope";
public override void Init()
{
base.Init();
log4net.Config.DOMConfigurator.Configure();
Castle.ActiveRecord.Framework.IConfigurationSource source =
System.Configuration.ConfigurationManager.GetSection("activerecord") as Castle.ActiveRecord.Framework.IConfigurationSource;
Castle.ActiveRecord.ActiveRecordStarter.Initialize(typeof(MythicalBugTracker.Model.User).Assembly, source);
this.BeginRequest += new EventHandler(MythicalBugTrackerApplication_BeginRequest);
this.EndRequest += new EventHandler(MythicalBugTrackerApplication_EndRequest);
}
void MythicalBugTrackerApplication_EndRequest(object sender, EventArgs e)
{
ISessionScope scope = Context.Items[SessionScope] as ISessionScope;
if (scope != null)
scope.Dispose();
}
void MythicalBugTrackerApplication_BeginRequest(object sender, EventArgs e)
{
Context.Items.Add(SessionScope, new SessionScope());
}
}
The utility methods I added are:
- ValidateObjectToSendToView - make sure that the object is not null (if yes, redirect to the NoSuchObject page) and push it to the view
- RenderNoSuchObject - display a generic error about missing object
- RenderDelete - render the cofirm delete page
- AddAllUsersToPropertyBag - make all the users avialable to the view
- RenderMessage - render the generic message (good, bad, debug, databind violations)
Note: You can now get the source for the Mythical Bug Tracker here, In order to run this code you need:
- New build of castle, revision 1700 and onward at least (just get the latest from Subversion, that should be enough).
- .Net 2.0
- SQL Server or SQL Express
Enjoy,
More posts in "Castle Demo App" series:
- (03 Mar 2006) ViewComponents, Security, Filters and Friends
- (01 Mar 2006) Code Update
- (01 Mar 2006) Queries and Foreign Keys
- (28 Feb 2006) Complex Interactions
- (25 Feb 2006) CRUD Operations on Projects
- (22 Feb 2006) Let There Be A New User
- (22 Feb 2006) Updating our Users
- (20 Feb 2006) Getting serious, the first real page
- (20 Feb 2006) MonoRail At A Glance
- (20 Feb 2006) The First MonoRail Page
- (19 Feb 2006) Many To Many Relations
- (19 Feb 2006) Lazy Loading and Scopes
- (17 Feb 2006) Active Record Relations
- (17 Feb 2006) Getting Started With Active Record
Comments
Comment preview