In this edition of our continuing series of blog posts on how to create a project management application in XAF, we will be progressing with the statuses functionality within our application. This time around, we’ll be adding a new task type, cleaning up our code a little bit and adding some logic around the rules of marking a task as complete.
The first thing we are going to do is to add a new engineering task type, with it’s own status set:
[DefaultClassOptions]
[NavigationItem("Planning")]
public class EngineeringTask : BaseObject {
public EngineeringTask(Session session) : base(session){ }
}
public enum EngineeringTaskStatus {
Draft,
Planned,
Elaboration,
Development,
Documenting,
Completed
}
Now that we have both a ProjectTask and an Engineering task, it stands to reason that there is going to be duplicate code, i.e. code that appears both in the ProjectTask and the EngineeringTask. The duplication of code can only lead to errors at maintenance time, and so we should create a common base class, from which each class can derive and place all the common code there, leaving the subclasses to house the specific task code:
public abstract class TaskBase : BaseObject
{
public TaskBase(Session session)
: base(session)
{
}
}
Now let’s move the common code from the ProjectTask into the TaskBase class:
[RuleCriteria("ProjectTask-EndDateMustBeAfterStartDate", DefaultContexts.Save, "EndDate > StartDate")]
public abstract class TaskBase : BaseObject {
public const string StatusSetName = "TaskBase";
protected override XPCollection<T> CreateCollection<T>(DevExpress.Xpo.Metadata.XPMemberInfo property) {
XPCollection<T> col = base.CreateCollection<T>(property);
if(property.Name == "TaskWork")
col.CollectionChanged += new XPCollectionChangedEventHandler(col_CollectionChanged);
return col;
}
protected void col_CollectionChanged(object sender, XPCollectionChangedEventArgs e) {
if(e.CollectionChangedType == XPCollectionChangedType.BeforeRemove) {
RemainingHours += ((TaskWork)e.ChangedObject).HoursSpent;
UpdateHoursSpent();
}
}
protected abstract string GetStatusSetName();
protected static void UpdateDatabase<EnumerationType>(Session session, string statusSetName) {
if(!typeof(EnumerationType).IsEnum)
throw new ArgumentOutOfRangeException();
TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
if(statusSet == null) {
string[] statusNames = Enum.GetNames(typeof(EnumerationType));
TaskStatus[] statusList = new TaskStatus[statusNames.Length];
statusSet = new TaskStatusSet(session, statusSetName);
TaskStatus nextStatus = null;
for(int j = statusNames.Length - 1; j >= 0; j--) {
string statusName = statusNames[j];
statusList[j] = new TaskStatus(session, statusSet, nextStatus, statusName);
nextStatus = statusList[j];
}
statusSet.FirstStatus = statusList[0];
statusSet.LastStatus = statusList[statusList.Length - 1];
statusSet.Save();
}
}
public TaskBase(Session session)
: base(session) {
}
public override void AfterConstruction() {
base.AfterConstruction();
this.StatusSet = FindTaskStatusSet(Session, GetStatusSetName());
this.Status = this.StatusSet.FirstStatus;
}
public static void UpdateDatabase(Session session)
{
throw new NotImplementedException("Inheritor class has to override (witn 'new' keyword) and implement this method");
}
public static TaskStatusSet FindTaskStatusSet(Session session, string statusSetName) {
return session.FindObject<TaskStatusSet>(new BinaryOperator("Name", statusSetName));
}
public void SetNextStatus() {
if(this.Status == this.StatusSet.LastStatus) {
throw new InvalidOperationException();
}
this.Status = this.Status.NextStatus;
}
//properties (unchanged)
...
}
So let’s see what it was that we did here. We didn’t apply the “DefaultClassOptions” and “NavigationItem("Planning")” attributes because the class cannot be instantiated and so shouldn’t have a navigation item. We’ve also introduced a new constant (StatusSetName) which identifies the status set corresponding to the current task type, descendants of this class will have to override this to specify their own StatusSet. Then, We defined a new function – UpdateDatabase, which will be used by TaskBase descendants to set the required status set types. As you can see to make sure that this method cannot be used unless overridden, we throw an exception in it. Having done that, we’ve marked the old UpdateDatabase method as protected and made it generic. The generic type parameter specifies the required status set. In the method we also check that the passed type is an enumeration. Also, the FindTaskStatusSet method now takes an additional parameter which specifies the status set name. We’ve added this new status set name parameter to the FindTaskStatusSet and UpdateDatabase methods because these method implementations reside in the base class, while the methods are going to be called from descendants which can use different status sets. To move the AfterConstruction method to the base class we’ve replaced the call to the StatusSetName property with a call to the GetStatusSetName method. This method is abstract – so, descendants have to implement it.
Now that we’ve defined our TaskBase class, let’s go back and define our new ProjectTask class:
[DefaultClassOptions]
[NavigationItem("Planning")]
public class ProjectTask : TaskBase
{
public new const string StatusSetName = "ProjectTask";
public ProjectTask(Session session)
: base(session)
{
}
public new static void UpdateDatabase(Session session)
{
UpdateDatabase<ProjectTaskStatus>(session, StatusSetName);
}
protected override string GetStatusSetName()
{
return StatusSetName;
}
}
public enum ProjectTaskStatus
{
Draft,
NotStarted,
InProgress,
Paused,
Completed
}
Looking at this code we can see what we have to do when we derive a new task type:
- Override the StatusSetName constant
- Implement the GetStatusSetName method
- Override the the public UpdateDatabase method to call the base protected UpdateDatabase method.
Now that we know what needs to be done, let’s go ahead and define our EngineeringTask class:
[DefaultClassOptions]
[NavigationItem("Planning")]
public class EngineeringTask : TaskBase
{
public new const string StatusSetName = "EngineeringTask";
public EngineeringTask(Session session)
: base(session)
{
}
public new static void UpdateDatabase(Session session)
{
UpdateDatabase<EngineeringTaskStatus>(session, StatusSetName);
}
protected override string GetStatusSetName()
{
return StatusSetName;
}
}
public enum EngineeringTaskStatus
{
Draft,
Planned,
Elaboration,
Development,
Documenting,
Completed
}
Oh, and we also have to change all the existing ProjectTask class references to the TaskBase class. If we now launch the application, we’ll be able to create a EngineeringTask object and see that it has all the fields of the TaskBase class, but the status set is differently (you can see the “Elaboration” status on the screenshot).

One thing though, most of the fields are declared in the TaskBase class, and so they are displayed in the “Task Base” (and not the “Engineering Task”) layout group. This looks a bit odd, so let’s correct it. To do that we’ll need to edit the Application Model for the module project. We’ll rename this layout group for the EngineeringTask and ProjectTask classes:
<DetailView ID="EngineeringTask_DetailView">
<Layout>
<LayoutGroup ID="Main">
<LayoutGroup ID="SimpleEditors">
<LayoutGroup ID="TaskBase" Caption="Engineering Task" />
</LayoutGroup>
</LayoutGroup>
</Layout>
</DetailView>
<DetailView ID="ProjectTask_DetailView">
<Layout>
<LayoutGroup ID="Main">
<LayoutGroup ID="SimpleEditors">
<LayoutGroup ID="TaskBase" Caption="Project Task" />
</LayoutGroup>
</LayoutGroup>
</Layout>
</DetailView>
Now, that looks a bit better, doesn’t it?

Okay, so the TaskBase class is abstract but we’ve still got to test it and to do that, we’re going to need a tests subclass. To create one we’ll create a new TaskBaseTests class and copy the ProjectTaskTests’s SetUp method to the TaskBaseTests, and change ProjectTask.UpdateDatabase(Session) to TestProjectTaskClass.UpdateDatabase(Session). Then move all the common functionality tests, changing the ProjectTask class name to TestProjectTaskClass and ProjectTaskStatus enumeration to TestProjectTaskStatus enumeration.
internal class TestProjectTaskClass : TaskBase
{
public new const string StatusSetName = "TestProjectClass";
public TestProjectTaskClass(Session session)
: base(session)
{
}
public new static void UpdateDatabase(Session session)
{
UpdateDatabase<TestProjectTaskStatus>(session, StatusSetName);
}
protected override string GetStatusSetName()
{
return StatusSetName;
}
}
public enum TestProjectTaskStatus
{
New,
InProgress,
Complete
}
Now that we moved all the common functionality tests to the TaskBaseTests class, we can modify the ProjectTaskTests class:
[TestFixture]
public class ProjectTaskTests : BaseXpoTest {
public override void SetUp() {
base.SetUp();
ProjectTask.UpdateDatabase(Session);
Session.CommitChanges();
}
[Test]
public void StatusSet_HasValueAfterConstruction() {
ProjectTask task = new ProjectTask(Session);
Assert.IsNotNull(task.StatusSet);
Assert.AreEqual(ProjectTask.StatusSetName, task.StatusSet.Name);
}
[Test]
public void UpdateDatabase_CorrectStatusSequence() {
TaskStatusSet statusSet = ProjectTask.FindTaskStatusSet(Session, ProjectTask.StatusSetName);
Assert.IsNotNull(statusSet);
TaskStatus currentStatus = statusSet.FirstStatus;
Assert.AreEqual(ProjectTaskStatus.Draft.ToString(), currentStatus.Name);
currentStatus = currentStatus.NextStatus;
Assert.AreEqual(ProjectTaskStatus.NotStarted.ToString(), currentStatus.Name);
currentStatus = currentStatus.NextStatus;
Assert.AreEqual(ProjectTaskStatus.InProgress.ToString(), currentStatus.Name);
currentStatus = currentStatus.NextStatus;
Assert.AreEqual(ProjectTaskStatus.Paused.ToString(), currentStatus.Name);
currentStatus = currentStatus.NextStatus;
Assert.AreEqual(ProjectTaskStatus.Completed.ToString(), currentStatus.Name);
Assert.AreEqual(null, currentStatus.NextStatus);
}
In the same way, we’d also create a test class for the EngineeringTask Class.
In the previous post we’d written an EasyTest script to ensure that the ChangeStatus button is inactive for completed tasks and active for the incomplete. It turns out that sometimes the button’s state doesn’t correspond to the current task’s status, so we need an additional test:
#DropDB XProjectEasyTest
#Application XProjectWin
#Application XProjectWeb
*Action Navigation(Project Tasks)
*Action Filter(All Tasks)
*ProcessRecord
Name = CompletedTask
!ActionAvailable Change Status
#IfDef XProjectWin
*Action Close
#EndIf
*Action Navigation(Project Tasks)
*ProcessRecord
Name = DraftTask
*ActionAvailable Change Status
Now if we run it we’ll see that it fails:
Application: XprojectWin Error in test: Action: 'Change Status' is available, line 13
Let’s take a closer look at the ChangeTaskStatusController’s code. Set a breakpoint inside the OnActivated and View_CurrentObjectChanged methods. Then launch the application and navigate between various tasks. After that try to open a task which has the “Completed” status. We can see that when a Detail View is open the View_CurrentObjectChanged event doesn’t fire. Because of this the UpdateChangeStatusActionState method isn’t called and the button remains active. Bummer.
To fix this, we’ll get rid of the CanChangeStatusController.UpdateChangeStatusActionState method and instead we’ll use the Action.TargetObjectsCriteria property:
public class ChangeTaskStatusController : ViewController {
public ChangeTaskStatusController() {
…
changeTaskStatusAction.TargetObjectsCriteria = "CanChangeStatus";
…
}
…
}
Now running the same test again shows that we have fixed the problem.
The last thing we are going to do in this post is to code up the logic that says that if a task is marked as “Complete” then it’s progress must be 100%. To support this behaviour we’ll amend the TaskBase’s Progress property:
public float Progress {
get {
if(this.Status == this.StatusSet.LastStatus)
return 1;
return TotalHours > 0 ? HoursSpent / TotalHours : 0;
}
}
And there we go, we’re all done for this post. If you want to follow along at home then you can download the code from here. In the next post we’ll refactor our status system so that a user can choose a status from a list of possible values. In the meantime, happy XAFing!