We have CRUD operations for users and projects. The next step is to add a way to assign / remove users from a project. We can do it both for users and projects, but this is just overly complex. We're going to allow a project edit page to add or remove users, and that is all.
I'll be the first to admit that I'm not very good with JavaScript, and I've practically no knowledge in the Ajax capabilities of MonoRail (which are based on Prototype library). I'm just going to stubmle around in this post, and see if I can make something work. Do ignore any screaming that you may hear in the background, this is just me hating browsers :-)
The reason that I'm so late with this post is that I run into a couple of problem with Ajax that weren't easy to solve (or even find). Just to put things in perspective, I wrote the code for this post in an hour and a half, and that included looking at MonoRail source, finding a bug and fixing it. In contrast, it took me much longer to write / debug / fix the problems in the java script parts. I think that this is mostly my fault, though, for not knowing the DOM and the environment well enough. Anyway, enough chit-chat, we've an application to write!
We start by adding this snipper to the head of Views\Layout\Admin.boo:
<head>
${AjaxHelper.GetJavascriptFunctions()}
<title>Administrator Section</title>
</head>
This simply add the correct javascript refeferences (<script> tag) so we could use the ProtoType functions. What I'm going to do next involve a bit of Ajax and some understanding of the tools that MonoRail gives me. I'll explain everything in a couple of minutes, but for now, here is the new EditProject method:
public void EditProject([ARFetch("id")]Project project)
{
AddAllUsersToPropertyBag();
ValidateObjectToSendToView("project", "Projects", project);
User[] allUsers = User.FindAll();
PropertyBag["assignedUsers"] = new User[0];
PropertyBag["unassignedUsers"] = allUsers;
if (project == null)
return;
ICollection<User> assignedUsers = project.Users;
User[] unassignedUsers = Array.FindAll(allUsers, delegate(User user)
{
return assignedUsers.Contains(user) == false;
});
PropertyBag["assignedUsers"] = assignedUsers;
PropertyBag["unassignedUsers"] = unassignedUsers;
}
This requires some explanation, I believe. The first two lines are the same as before, but the rest of it is all new. The idea is to pull all the users from the database, and then filter them based on those not already assigned to the project. We then pass the assigned and unassigned users to the view. The two lines just after User.FindAll() are there because we need to give the view something to display if this is a brand new project.
Now, let's see what we need to do to the view in order to make it support our brand new interface, here we have quite a bit of word, as a matter of fact. Because I made so many changes, I'm going to show each part of the page and then provide explanation on what it does. It's a lot of code, but it is not complicated one:
<?brail
def outputUsersList(usersList, addEnabled, removeEnabled):
index = 0
addStyle = ""
removeStyle =""
addStyle = 'display:none;' if addEnabled == false
removeStyle = 'display:none;' if removeEnabled == false
for user in usersList:
userClient = "user_id[${user.Id}]"
moveToAssigned = "addToProject($('${userClient}'))"
moveToUnassigned = "removeFromProject($('${userClient}'))"
output "<tr id=${userClient} userId='${user.Id}'><td>"
output AjaxHelper.LinkToFunction('<<', moveToAssigned, {'id':userClient+'.addToProject', 'style':addStyle})
output "</td><td>${user.Name}</td><td>"
output AjaxHelper.LinkToFunction('>>', moveToUnassigned, {'id': userClient+'.removeFromProject', 'style':removeStyle})
output "</td></tr>"
index++
end
end
name = ""
defaultAssigneeId = -1
id = "0"
if ?project:
name = project.Name
defaultAssigneeId = project.DefaultAssignee.Id
id = project.Id.ToString()
end
?>
Notice the outputUsersList() method, this one is one of the reasons I love Brail, it allows me to do Extract Method refactoring for HTML. What this method does is very simple, it gets a list of users, and two flags telling it whatever to show or hide the add / remove functionality. You can see a couple of things here, we are taking advantage of the fact that Boo (which is the base langauge for Brail) has strong inference capabilties, so we don't bother with specifying types, in fact, this method declaration looks very much like the declaration we will see in a moment, in the java script code. (Although I find Boo much easier to work with than Javascript, of course.)
We output some HTML to the client, which isn't very interesting (and vaguely resembles CGI code), but we have a call there to AjaxHelper, which I'm going to assume that you're interested at. The method we use is LinkToFunction(), which just means that MonoRail generates a link for us, and will call a (local) java script function when the link is clicked. The first parameter is the text of the link, the second is the function to call (including its parameters), the third is used to pass a dictionary of attributes for the link (and one of them is display=none, which is used to hide the link if it's not needed.
One thing to notice here is the way I'm using the javascript. First, "$()" is a function call that I get from Prototype, it'll get me the element that I need without needing to do stuff like document.getElementById(), etc. Second, I'm passing this function the id for whole <tr> element. We'll see how it makes our life easier in a couple of minutes, since it means that I can just move stuff around without worrying how it looks. Third, notice that I'm naming all the links the same way, the container id plus "addToProject" or "removeFromProject", this will allow me to get the correct link easily, without traversing the DOM to find the right one. Because I know the user id is unique, I know that both the <tr> id and the links ids will be unique in the page. One other thing that I am doing here is to add a userId attribute to the <tr> element, I'll be using this attribute in the Ajax code, to get the user I'm working on.
I think that the parts after the method require no explanation, we've already seen them before. Before I will show the Javascript, we need to take a look at the page, and see how it's laid out, otherwise you wouldn't be able to make head or tails from the scripts, so here it is:
${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>
<p>Users Assignments:</p>
<div id='assignedUsersInputs'>
<?brail for user in assignedUsers: ?>
${HtmlHelper.InputHidden('project.Users', user.Id)}
<?brail end ?>
</div>
<table>
<tr>
<th style="BORDER-BOTTOM: #f96 1px dashed; BORDER-RIGHT: #f96 1px dashed">Users assigned to this project:</th>
<th style="BORDER-BOTTOM: #f56 1px dashed; BORDER-LEFT: #f56 1px dashed">Users not assigned to this project:</th>
</tr>
<tr>
<td style="BORDER: #f96 1px dotted">
<table id="assignedUsersTable">
<tbody>
<?brail
outputUsersList(assignedUsers, false, true)
?>
</tbody>
</table>
</td>
<td style="BORDER: #f56 1px dotted">
<table id="unassignedUsersTable">
<tbody>
<?brail
outputUsersList(unassignedUsers, true, false)
?>
</tbody>
</table>
</td>
</tr>
</table>
<p>
${HtmlHelper.SubmitButton('Save')}
</p>
${HtmlHelper.EndFieldSet()}
${HtmlHelper.EndForm()}
The first part isn't interesting, since we already seen it before, what we will look at is everything below the "Users Assignments" paragraph. We have the assignedUsersDiv, where we put all the users assigned to this project in hidden inputs. Notice that all those hidden input fields are named the same: "project.Users", this will become important when we'll save the project.
We then define a table, and use the method outputUsersList() to output the correct list to the page. If we would run the page now, we'll see something like this:
I am not a designer, and the HTML above has some hardocded values instead of using CSS, but I'm not doing this series to show you how to build pretty sites. :-) Do you see the >> and << signs near the users? This will allow us to simply move a user to or from the project.
Assume for a moment that this page was written in ASP.Net, and that a post-back is required to move a user on/off the project. I now want to add three users and take one away, how much time will this take me? About a minute or two, assuming that the connection and server are fast. And that is for doing no processing whatsoever excpet move a text box from one spot to the other.
The way we are doing this is much more user friendly (at the cost of being much less developer friendly, but that is the usual balance). Okay, we've seen the HTML code and the way it looks like, now let's move on to everyone's favoriate buzzward, Ajax:
<script lang="javascript">
function addToProject(userTag)
{
Element.remove(userTag);
$('assignedUsersTable').tBodies[0].appendChild(userTag);
toggleLinks(userTag);
var i =0;
var hidden = document.createElement('input')
hidden.type='hidden';
hidden.value = userTag.getAttribute('userId');
hidden.id = hidden.name = 'project.Users';
$('assignedUsersInputs').appendChild(hidden);
}
function removeFromProject(userTag)
{
Element.remove(userTag);
$('unassignedUsersTable').tBodies[0].appendChild(userTag);
toggleLinks(userTag);
for(var node = $('assignedUsersInputs').firstChild;node !=null; node = node.nextSibling)
{
if(node.nodeType != 1) //input
continue;
if(node.value == userTag.getAttribute('userId'))
{
Element.remove(node);
break;
}
}
}
function toggleLinks(userTag)
{
Element.toggle($(userTag.id +'.addToProject'),
$(userTag.id +'.removeFromProject'));
}
</script>
We'll start with the simplest function, toggleLinks(), it takes a single element, and using its id, it find the related addToProject and removeFromProject links, and toggle their visibility. The Element class is part of Prototype, and provides many useful methods.
The addToProject() function will remove the element it gets from the page and add it to the assignedUsersTable and switch the links state. Then it creates a hidden input field and stores it in the originally named assignedUsersInputs <div>. This <div> is used merely as a place holder in the DOM for all the hidden fields. One super important thing to remember. An input field you create with javascript must have a name, otherwise it won't be sent to the server! I lost more than a little bit of time over this. (Thanks, Jeff!)
We're setting all the input fields to use the same name and id. The id I don't care much about, but it's important that they'll all have the same name, since we'll use that in our controller code.
The removeFromProject() function does pretty much the same thing, physically move the element from one container to another, toggle the links and then search through the children of assignedUsersInputs for the hidden field that match the user, so it can remove it.
Okay. This is it. We're done with all the client code! I'm thinking about throwing a party just for that...
Now to the SaveProject() method, which should now handle saving the assigned users for this project. How hard do you think this will be? Let's find out:
public void SaveProject(
[ARDataBind("project", AutoLoadUnlessKeyIs = 0)]Project project,
[ARFetch("project.DefaultAssignee.Id",
Create = false, Required = true)] User defaultAssigneee,
[ARFetch("project.Users")]User[] assignedUsers)
{
RenderMessage("Back to Projects", "Projects");
try
{
if (project.DefaultAssignee == null)
project.DefaultAssignee = defaultAssigneee;
//Remove all assigned users from the project, since the autority
//on who is assigned to the project is now the assigned users in the page.
project.Users.Clear();
Array.ForEach(assignedUsers,
delegate(User users) { project.Users.Add(users); });
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();
}
}
That is a lot of attributes, I head you say, and I agree. But it's better than saying, that is a lot of code... Those attirbutes saves quite a bit of time, and if you don't like them, you can access their functionality without losing anything. Personally, I like this ability, but I know that opinions about this are divergent.
What do we have here, then? We have the project and defaultAssignee parameters, which we had before, and a new one, an array of assignedUsers. I talked about ARFetch in previous posts, it take an id and gives you the matching object. It also works on arrays, so I don't need to do anything to get the list of users the admin assigned to this project. ARFetch knows to do this because all those ids were named project.Users, so it can just grab all the parameters with this name and load them by their ids.
One thing that I have to do manually, however, is to add the assigned users to the project. MonoRail will not do this for me, and that is a Good Thing. MonoRail doesn't know about the semantics of the operation, in this case, I want to remove all the users already assigned, and add the ones the admin assigned now, but what happens when I want something else? MonoRail can't tell, so it's giving us the live objects, and leave the last step for the business logic to handle.
As you can see, it's not much of a burden, we clear the Users collection, then add all the users the admin specified to the project and save the project. That is nearly it. We need to pass the assignedUsers and unassignedUsers to the view from the CreateProject action as well. Since I'm going to be duplicating code here, I'm going to refactor it out and put it in AddAllUsersToPropertyBag(), since this is the common dominator between EditProject() and CreateProject().
Okay, that was a long post. Just a couple of things before I wrap things up, just think of how much work we did in this page. We've an Ajax interface, we load and save complex data to the database, including managing many to many relationships. I may be a partial observer, but I think that we did all that with a minimum of fuss, and not much complexity.
Next, we will talk about some latent bugs and adding some more validation to the application. Then, it'll be time for the Big Security Review. I got another post coming soon, so I'll update the binaries on the site when I post that one, and not for this.