2006-12-02

Reverse derived columns

The focus of this post will be "Event derived columns". Jan Nordén (Borland) pointed me in the direction of these things recently when I asked him how to solve a GUI problem I had. When I used Bold for Delphi there was this really nice GUI component called a BoldSelectionListBox. This component would let me show a list of items with a CheckBox next to each row, ticking / unticking a box would add / remove an association between the object selected and some other object of my choice.

Using this BoldSelectionListBox I would be able to specify a User (for example) as the context and then have a list of all Groups in a kind of CheckListBox. Ticks would appear in all CheckBoxes where the User is belongs to the group listed, and no tick where they are not part of the group. The extra clever part of course is that by ticking a CheckBox Bold would create the link object required to tie the User to the Group, and add it to User.Groups (and of course Group.Users).

Seeing that ECO does not introduce any GUI controls (it instead provides .net DataBinding interfaces so that you can use standard controls), I suspected that I would not be able to achieve the same sort of effect. Jan kindly sent me a small demo showing how to achieve this using only DataGrids. I soon had this logic written into my own app, and it worked beautifully!

I added expression handles for my Groups (ehGroups) and Users (ehUsers), linked them up to a grid each and added Add / Delete buttons. I set each of these expression handles to retrieve all instances, "Group.allInstances" and "User.allInstances".

On the left side of my GUI I had all of my Users listed, and on the right I had all of my Groups. I now wanted to add a CheckBox next to each Groups, so that I could specify whether the user belonged to the Group or not. The first problem to tackle is to know which User is currently selected in the grid. To do this I added a CurrencyManagerHandle named "chCurrentUser", set its RootHandle to ehUsers, and its BindingContext to UsersDataGrid. Now chCurrentUser holds a reference to the current User, nice and easy.

Next I needed to get a CheckBox column in my GroupsDataGrid and set AllowNull to False. To do this I added a GroupSelected column to ehGroups and set its type to System.Boolean. Note: The "Add" button in the Columns editor has a DropDown icon next to it, click that and select EventDerivedColumn. I then added the additional column to my GroupsDataGrid, to make sure it was a CheckBox I chose the DropDown list on the Add button and selected DataGridBoolColumn. I set the MappingName to GroupSelected.

So far we have everything we need to see the CheckBoxes, but no way to tell the DataGrid whether the checkbox should be ticked or not. To do this we need to write some code into ehGroups' DeriveValue event, but first I want to add something to make the code a little easier to write. I added a new ExpressionHandle ehUserGroups, the RootHandle was the CurrencyHandle (chCurrentUser) and the expression was "self.groups". This would allow me to easily check which Groups the current user belongs to.

Now to write some code to calculate the value of ehGroups.GroupSelected. This is done in the ehGroups.DeriveValue event, like so:
private void ehGroups_DeriveValue(object sender,
Borland.Eco.Handles.DeriveEventArgs e)
{
switch (e.Name) //One event for all derived columns
{
case "GroupSelected":
//Get a list of allowed groups for this task
IElementCollection groups =
(IElementCollection) ehUserGroups.Element;

//Avoid a null reference exception
if (groups == null)
{
//return an element representing the constant "false"
e.ResultElement =
EcoSpace.VariableFactoryService.CreateConstant(false);
return;
}

//Observe the ehUserGroups element, this tells us
//when the element changes so that we may
//invalidate the GUI
ehUserGroups.SubscribeToElement(e.ResubscribeSubscriber);

//Also observe the items within the list
groups.SubscribeToValue(e.ValueChangeSubscriber);

//If user.groups contains the current Group then return
//an element representing the constant "true"

if (groups.Contains(e.RootElement))
e.ResultElement =
EcoSpace.VariableFactoryService.CreateConstant(true);
else
//Otherwise return an element
//representing the constant "false"
e.ResultElement =
EcoSpace.VariableFactoryService.CreateConstant(false);

break;

default:
throw new NotImplementedException(e.Name + " not derived properly");
}//switch
}//ehGroups_DeriveValue
And finally we need to have a way to allow the user to tick / untick a CheckBox and have the relevant association added or removed from the user.groups association. This is done in the ehGroups.ReverseDeriveValue event, like so
private void ehGroups_ReverseDeriveValue(object sender,
Borland.Eco.Handles.ReverseDeriveEventArgs e)
{
switch(e.Name) //One event for all derived columns
{
case "GroupSelected":
//Get a list of current groups for the user
IElementCollection groups =
(IElementCollection) ehUserGroups.Element;

//Avoid a null reference exception
if groups == null)
return;

//Typecast the value being set to
//a Boolean (from the datagrid CheckBox)
if ( (Boolean) e.Value)
{
//If the checkbox has been checked,
//and the ticked Group is not in
//user.groups then add it
if (!groups.Contains(e.RootElement))
groups.Add(e.RootElement);
}
else
{
//If the checkbox has been unchecked,
//and the ticked Group exists in
//user.groups then remove it
if (roles.Contains(e.RootElement))
groups.Remove(e.RootElement);
}
break;
}//switch
}//ehGroups_ReverseDeriveValue
It may take a little bit of getting used to, but if you read it through a few times you should be able to get the jist of it. This basically gives the developer the power of reversed derived attributes for use solely within the GUI. This means that we can do some clever things with ECO objects without having to include reverse derived attributes in the model in order to satisfy GUI requirements.

No comments: