Blogs

Gary's Blog

September 2009 - Posts

  • XAF – Project Management Application #10

         

    Phew, well I’m back from a hectic week at BASTA and all set to post the latest in our series on creating an project management application using XAF. So, what are we going to do in this post? Well, first of all our status changing algorithm isn’t very good is it? For a start you can’t go backwards to a previous status and it assumes that their is only one possible status to go forward to. These two things make our model of status pretty unrealistic. So, let’s do something about that.

    A state machine or workflow solution sounds best suited to fixing this problem and, as I’ve said, there are plans for such things, but for now we’ll allow users to decide which status suits best. We’ll get rid of the “Change status” button and make the status list editable. To do this, we’ll remove the “protected” keyword from the TaskBase’s Status property setter declaration.

    Now we’re able to change the status manually:

    1

     2

    Okay, well that sort of worked, but there are a couple of issues:

    1. The New and Clear buttons are displayed in the lookup.
    2. Only the statuses corresponding to the current task type should be displayed, not all of them.
    3. Statuses are sorted in the alphabetical order instead of their logical sequence;

    So, let’s write a test for the New button to ensure it’s unavailable:

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Project Tasks)

    *Action New

    #IfDef XProjectWin

    *ExecuteEditorAction Status

    !ActionAvailable New

    #EndIf

    #IfDef XProjectWeb

    !ExecuteEditorAction Status(Add)

    #EndIf

    Looking at this test, you can see that the syntax is different between the web and winforms actions; oops. :-) Don’t worry we are aware of this and will fix it. Running that test at the moment fails, not surprisingly. To remove the New button we’ll create a new AvailableStatuses property on the TaskBase Class:

    public abstract class TaskBase : BaseObject {
    …
        private XPCollection<TaskStatus> availableStatuses;
    
        [Browsable(false)]
        public XPCollection<TaskStatus> AvailableStatuses {
            get {
                if(availableStatuses == null) {
                    availableStatuses = new XPCollection<TaskStatus>(StatusSet.Items);
                    availableStatuses.BindingBehavior = CollectionBindingBehavior.AllowNone;
                }
    
                return availableStatuses;
            }
        }
    …
    }

    Check out the [Browsable(false)] attribute above, using this means that the property will not be visible in the UI. Meantime, the AvailableStatuses field will hold a local status collection. We’ll set this property’s BindingBehavior to CollectionBindingBehavior.AllowNone, so that the New button won’t be available in the AvailableStatuses lookup. Also note that the availableStatuses field is initialized only once when its value is first accessed. Now let’s limit the statuses displayed in the lookup to those that belong to the current task type; to do that we apply the DataSourceProperty("AvailableStatuses") attribute to the Status property:

    [Persistent, DataSourceProperty("AvailableStatuses")]
    public TaskStatus Status {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        set { SetPropertyValue<TaskStatus>("Status", value); }
    }

    Launching the application, we see that the “new” button has gone and the status list looks much better:

    3

    4

    Running the functional test, above, now passes as a result of this change. :-)

    The next thing we are going to do is to nail that “Clear” button problem, we need to remove it so that the Status can’t be set to NULL. We’ll also write a unit test to check this:

    [Test]
    public void Status_CannotAssignNull() {
        TestProjectTaskClass task = new TestProjectTaskClass(Session);
        Assert.IsNotNull(task.Status);
    
       try {
            task.Status = null;
            Assert.Fail();
        }
        catch (ArgumentNullException){ }
    }

    No surprise, the test fails. So let’s modify the Status property a little:

    public TaskStatus Status
    {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        set
        {
    
            if (!IsLoading)
                Guard.ArgumentNotNull(value, "Status");
    
            SetPropertyValue<TaskStatus>("Status", value);
        }
    }

    Running the test again shows it passes. Ah, don’t you just love the little victories? :-)

    Now we need to remove the “Clear” button from the Windows Forms application. To do this we’ll add a new TaskStatusLookupController View Controller. We want this Controller to be activated only for the Status lookup and so we’ll set its TargetViewId property to TaskStatus_LookupListView. We’ll write the code to remove the “Clear” button in the Controller’s OnActivated method:

    public class TaskStatusLookupController : ViewController
    {
        public TaskStatusLookupController()
        {
            TargetViewId = "TaskStatus_LookupListView";
        }
    
        protected override void OnActivated()
        {
            base.OnActivated();
    
            LookupWindowController standardController = Frame.GetController<LookupWindowController>();
            if (standardController != null)
                standardController.Active.SetItemValue("TaskStatusLookupController", false);
        }
    }

    As you can see, the “Clear” button is now gone:

    5

    Now, let’s go ahead and remove the “Clear” button from the ASP.Net application. Umm… yeah… Turns out there’s no easy way to do this at the moment, so we’ll create a suggestion for the XAF team to write such functionality, and quietly move on. :-)

    Moving on… let’s fix the issue of the alphabetical order of the statuses. We’ll replace our enumerations with string arrays that contain statuses in the required order and we’ll modify the UpdateDatabase method in the TaskBase class to achieve this:

    protected static void UpdateDatabase(Session session, string[] statusNames, string statusSetName)
    {
        TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
    
        if (statusSet == null)
        {
            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();
        }
    }

    We also need to change the initialisation code of our Task:

    public class ProjectTask : TaskBase {
        public static readonly string[] InitialStatusNames = new string[] { "Draft", 
            "NotStarted", 
            "InProgress", 
            "Paused", 
            "Completed" };
    …
        public new static void UpdateDatabase(Session session) {
            UpdateDatabase(session, InitialStatusNames, StatusSetName);
        }
    …
    }

    Having made this change, I’ll leave you to remove the enumerations and amend the unit tests accordingly. Also, we need to amend the AvailableValuesForTaskStatus functional test. Change:

    !FillForm

    Status = Completed

    To:

    *FillForm

    Status = Completed

    To change the sort order of the statuses in the lookup we’ll add an Index property to the TaskStatus class. Since we are going to use indexes instead of an ordered list, we’ll remove the Next property. In the TaskStatus constructor we’ll replace the nextStatus parameter with the index parameter:

    public class TaskStatus : BaseObject {
     
        public TaskStatus(Session session, TaskStatusSet owner, int index, string name) : base(session) {
            Guard.ArgumentNotNull(owner, "owner");
            Guard.ArgumentNotNullOrEmpty(name, "name");
            Owner = owner;
            Name = name;
            Index = index;
        }
    
        public int Index {
            get { return GetPropertyValue<int>("Index"); }
            set { SetPropertyValue<int>("Index", value); }
        }
    ...
    }

    We’ll also initialize the new field in the TaskBase class’ UpdateDatabase method:

    protected static void UpdateDatabase(Session session, string[] statusNames, string statusSetName)
    {
        TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
    
        if (statusSet == null)
        {
            TaskStatus[] statusList = new TaskStatus[statusNames.Length];
            statusSet = new TaskStatusSet(session, statusSetName);
    
    
            for (int j = 0; j < statusNames.Length; j++)
            {
                statusList[j] = new TaskStatus(session, statusSet, j, statusNames[j]);
            }
    
            statusSet.FirstStatus = statusList[0];
            statusSet.LastStatus = statusList[statusList.Length - 1];
    
            statusSet.Save();
        }
    }

    Now we must get rid of the NextStatus button and the TaskBase class’ SetNextStatus method. We’ll remove this method as well as the ChangeTaskStatusController Controller and we’ll remove some unit tests which are now useless. For this purpose we’ll update and move the correct status order checking logic from the descendant tasks tests to the TaskBase tests:

    [Test]
    public void UpdateDatabase_CorrectStatusSequence()
    {
        TaskStatusSet statusSet = TestProjectTaskClass.FindTaskStatusSet(Session, TestProjectTaskClass.StatusSetName);
        Assert.IsNotNull(statusSet);
    
        List<string> statusList = new List<string>(TestProjectTaskClass.InitialStatusNames);
        foreach (TaskStatus currentStatus in statusSet.Items)
        {
            Assert.AreEqual(statusList.IndexOf(currentStatus.Name), currentStatus.Index);
        }
    }

    Now we’ll configure the order of the items in the lookup editor for the status property. In the Model Editor open the TaskStatus_LookupListView node and add a new column for the Index property, disable sorting for the Name property and enable sorting for the Index property (set the required value for the SortOrder attribute):

    6

    Right, so let’s fire up our applications now:

    7

     8

    Yay! The winform application looks great and the webform application… meh, not so much. Looks like we’ve found a bug. The list of objects in Simple mode (ComboBox edit) is always sorted by Name regardless of what the settings in ListView node are.

    Well, while that bug is getting fixed we’ll go ahead and introduce the capability to modify/edit status sets from the UI. First, we’ll apply the [NavigationItem("Settings")] attribute to the TaskStatusSet class this will mean that we can now edit the StatusSet objects in the UI:

    9

     10

    Hmm, well it’s a start but there are still issues: the Name property is editable, which can cause a mismatch between a task type and the corresponding status set. To fix this, we’ll make the TaskStatusSet’s Name property setter protected. Additionally we’ll apply the Persistent attribute, so that XPO will persist this property:

    [Persistent]
    public string Name
    {
        get { return GetPropertyValue<string>("Name"); }
        protected set { SetPropertyValue<string>("Name", value); }
    }

    Also, the “Task Status Set” caption in the navigation control isn’t very useful. We’ll rename it “Task Statuses”. To do this, add the DisplayName attribute to the TaskStatusSet class:

    [NavigationItem("Settings"), System.ComponentModel.DisplayName("Task Statuses")]
    public class TaskStatusSet : BaseObject
    {
    …
    }

    Next, the default layout of controls isn’t very convenient. For example, the Name property should be displayed first. We’ll use the Model Editor to fix this. Double click the Model.DesignedDiffs.xafml file in the module project, navigate to the Application\Views\TaskStatusSet_DetailView\Layout node. Right click the design surface and choose “Customize Layout”:

    11

    Statuses should be sorted by their indexes. To configure the sort order we’ll use the Model Editor (again <grin>). If we navigate to the TaskStatusSet_Items_ListView node, we’ll see that by default the Name property is used for sorting purposes (as it’s the default property). We want to sort by the Index property. To achieve this, change the SortIndex and SortOrder attributes’ values for the Name and Index properties. To check the default sorting we’ve written a functional test - StatusSet_Items_DefaultSortByIndex.ets which now passes:

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Task Statuses)

    *ProcessRecord

    Name = ProjectTask

    *CheckTable Items

    Columns = Index

    Row[1] = 0

    Row[2] = 1

    Row[3] = 2

    Row[4] = 3

    Row[5] = 4

    Users shouldn’t be able to create/delete StatusSets, we should only allow editing.Again we invoke the Model Editor and set the AllowNew and AllowDelete attribute to False for the TaskStatusSet_ListView and TaskStatusSet_DetailView Views. And again we write a functional test to check that the New and Delete Actions are disabled (StatusSet_DisabledNewDeleteActions.ets script file):

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Task Statuses)

    !ProcessRecord

    Name = ProjectTask

    Action = Delete

    !ActionAvailable TaskStatusSet.New

    ;using ProjectTask's existing statuses

    *ProcessRecord

    Name = ProjectTask

    !ActionAvailable TaskStatusSet.Delete

    We still need to filter the drop-down lookups for the FirstStatus and LastStatus properties. To do this we’ll apply the [DataSourceProperty("Items")] attribute to both properties of the TaskStatusSet class:

    public class TaskStatusSet : BaseObject
    {
    …
    
        [DataSourceProperty("Items")]
        public TaskStatus FirstStatus {
            get { return GetPropertyValue<TaskStatus>("FirstStatus"); }
            set { SetPropertyValue<TaskStatus>("FirstStatus", value); }
        }
    
        [DataSourceProperty("Items")]
        public TaskStatus LastStatus {
            get { return GetPropertyValue<TaskStatus>("LastStatus"); }
            set { SetPropertyValue<TaskStatus>("LastStatus", value); }
        }
    }
    Phew, well I don’t know about you, but I’ve had quite enough for this post. If you want to play along at home, you can download the code from here. Next time, we’ll integrating his application with another internal application, in the meantime, if you are doing anything interesting with XAF or XPO drop me a line and tell me all about it and… happy XAFing!
  • Sorry I’m not Here Right Now, Please Leave a Message After the Tone…

         

    As you may have guessed from the title, I’m not around right now. In fact I’ll be exhibiting the DevExpress product line at the BASTA conference in Mainz, Germany. If you are going to be attending the conference please stop by the booth and say hello, it’s always nice to meet readers and “put a face to a name”. If you are not going to be in Germany for the conference, then you can keep up to date with what is going on there by following me on Twitter (@garyshort).

    Normal service, well as normal as it gets around here anyway, will be resumed next Monday.

  • Successful Companies Follow Their Customers

         

    By 2010 the baby boomers will have given way to Generation Y and so your target marketing demographic will have moved on to a generation who are far more au fait with social technology than the generation that preceded them (96% of them have joined a social network). What will that mean for your future marketing strategies? Well for one thing, you are going to have to move your marketing online, if you haven’t already. Social networking has overtaken pornography as the #1 activity on the Internet, that’s where your customers are and that’s where you have to be too.

    Not only are your customers moving online, but the rate at which technology is reaching critical mass is increasing: whist it took radio 38 years to gain 50 million users, Facebook gained 100 Million users in less than 8 months and, incredibly, Apple gained 1 billion downloads from its AppStore in just 9 months. You may not think your company/product needs to be on Facebook, but with its 300 million active users, if it were a country, it would be the fourth largest; why would you want to shut yourself off from a market of that size?

    Meh, I hear you say, “we’re already online, we have a blog, Google knows where we are and therefore our customers know where we are” Really? Well that might be true, but did you also know that YouTube is the second largest search engine in the world? You have to think beyond producing the same bland, text based content as your competitors; you must start to innovate with other media, like video for example, to get your message across. Watching video content online is popular; Hulu, for, example, has grown from 63 million streams to 373 million in 1 year.

    Of course, when I say you must move your marketing online in order to reach the next generation of the target advertising demographic, I don’t just mean advertise online. Why not, I hear you ask? Simple, no one believes online advertising. Well, okay, that might be a bit of an exaggeration; the fact is only 33% of consumers trust online banner ads, versus 90% who trust word of mouth recommendations from friends. So the message is clear, if you want to sell to the next generation you have to talk to them.

    Talk to them? But when? Where? Well the answer to that is simple: anytime and anywhere. 17% of Brits and 15% of Americans now social network via mobile phone your customers are never more than a few key presses away from their blogs; think about that the next time one of your staff members upsets a customer. There are nearly 200,000,000 blogs on the Internet with 54% of bloggers posting daily and 26% of bloggers blogging about products or brands. That means there is someone talking about your products. The time for deciding if you want your product to be talked about has gone – the only thing left to decide is: do you want to try and shape that conversation?

    Still not convinced that social media is the future of advertising and therefore the future of your company? What about print advertising I hear you say, that still works… doesn’t it? Not so much. Newspaper readership fell from 34% to 25% between 2006 and 2009 and the trend lines have been heading south for years. The truth is the smart money has moved online.

    Okay, so I’ve convinced you that you have to at least “dip your toe in the water” of social marketing, the question now is: how do you get started? Well, there are some great books here, here and here. Read them, put the lessons learned into practice and you’ll not go too far wrong. You should also check out these great blogs:

    http://sethgodin.typepad.com/
    http://www.chrisbrogan.com/
    http://www.steverubel.com/
    http://blog.guykawasaki.com/
    http://www.buzzmachine.com/

    But in the end, the best advice I can offer you is to get out there and do it, be honest, and just talk to your customers; they’re already talking about about you. :-)

  • XAF – Project Management Application #9

         

    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:

    1. Override the StatusSetName constant
    2. Implement the GetStatusSetName method
    3. 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).

    1

      2

    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?

    3

     4

    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!

  • Cloud Computing – It’s that New Old Thing

         

    Although the arguments over which was the first “real” computer rage on: is it the Zuse Z3, from Germany which was Turing complete but lacked conditional branching; is it the Colossus Mark 1, from the UK, which was programmable but not Turing complete; is it the ENIAC, from the US, which was both programmable and Turing complete but did not store any programmes onboard, or perhaps it was the Manchester Small-Scale Experimental Machine (or Manchester Baby as it was known), also from the UK, which was programmable, Turing complete and did store programmes onboard. Whichever is your personal choice, one thing is clear, they were eye-wateringly expensive.

    How expensive? Well, in 1956, the Farranti Pegasus sold for 50,000 GBP, that was when the average house in the UK would set you back a mere 2,500 GBP. To put that in context for you, that would mean that today the average computer would cost you just over 3,000,000 GBP! Of course, back then, there was no such thing as an “average” computer. Not every household had one, in fact not even every business had one. To demonstrate just how rare they were, Ferranti only sold 26 Pegasus 1 and 12 Pegasus 2 computers – of course, that still made it Ferranti’s most popular computer.

    So, if there were so few computers back then, how did computerisation work? Well, simply, you outsourced your computing entirely to something called a computer bureau. The bureau was there for companies that had neither the financial wherewithal to purchase their own computers, nor the expertise to program, operate and maintain such purchases. They worked by offering economies of scale; for a price, you could send your raw payroll data, for example, to a bureau and they would run the programs through the computer and return you the payroll output data.

    Of course the downside of this was fairly obvious: you had to transport your data to the bureau; their computers had to, reliably, run the calculations – this at a time when an engineer had to carry out an hour of preventative maintenance on the computer every morning before anyone went near it! Finally, the finished output data had to be returned to you. All this had to happen without a single error, if any part failed then the whole failed and you were left without your data. Bad news if it was payroll data and you couldn’t pay your workers. Of course, actions in mitigation had to be devised – what would you do if you didn’t get your payroll data back in time? Well you could just pay last month’s payroll, as a temporary measure, and make the required adjustments in the following month.

    Fast forward a couple of decades and the micro-processor comes on the scene. Computers plummet in price and every company who wants one can have one. They’re used for everything in the enterprise, from calculating the all important payroll, to the even more important managing the company’s lottery syndicate. Then something odd happens. Just as hardware is becoming simpler and cheaper we go and boik it all up by making the software more complex and more expensive. We add layer upon layer of abstraction; we have web servers talking to messaging servers talking to application servers talking to database servers and so on and so forth until the average company, not specialising in computing, throws up their hands and cries “Stop! Can’t I just outsource this into the cloud or something?”

    And with that we’ve come full circle. Now, for a price, you can send your raw payroll data, for example, to a SaaS application in the cloud and it will do the calculation and return you the payroll output data. Of course, you may wish to have your own programs for your enterprise, but you might not want to be saddled with the cost of maintenance of the application and storage servers. Well cloud computing can help you there too. Just purchase compute time and storage facilities as you require, and spin up more servers over peak times, and spin them down again when you’re done. Maximum flexibility minimum cost.

    Of course the downside of this was is fairly obvious: you had to transport your data to the bureau have to send your data to the cloud; their computers had have to, reliably, run the calculations – this at a time when an engineer had to carry out an hour of preventative maintenance on the computer every morning before anyone went near it! ISP can lose connectivity for days at a time. Finally, the finished output data had has to be returned to you. All this had has to happen without a single error, if any part failed fails then the whole failed fails and you were are left without your data. Bad news if it was is payroll data and you couldn’t can’t pay your workers. Of course, actions in mitigation had have to be devised – what would will you do if you didn’t can’t get your payroll data back in time? Well you could can just pay last month’s payroll, as a temporary measure, and make the required adjustments in the following month.

    Wait a minute… isn’t this all starting to sound rather familiar? If, like me, you think it is – then maybe it’s time to put a Beatles track on the turntable and shout viva la ‘60s!!

  • XAF – Project Management Application #8

         

    Welcome to part 8 of a series of blog posts illustrating the building of our project management application. In this post we’ll continue our work with Project statuses.

    Currently a user can change from one status to another without any consideration as to whether that change makes sense or not. This behaviour is not ideal, so we want to be able to specify which changes are valid and, as we already have the ProjectTaskStatuses enumeration, we’ll be using that to specify the order of the changes. The first thing we are going to do is to add the NextStatus property to the TaskStatus class:

    [Persistent]
    public TaskStatus NextStatus
    {
        get { return GetPropertyValue<TaskStatus>("NextStatus"); }
        protected set { SetPropertyValue<TaskStatus>("NextStatus", value); }
    }

    Then we’ll modify the constructor so that it takes the next valid Status as a parameter:

    public TaskStatus(Session session, TaskStatusSet owner, TaskStatus nextStatus, string name)
        : base(session)
    {
        Guard.ArgumentNotNull(owner, "owner");
        Guard.ArgumentNotNullOrEmpty(name, "name");
        Owner = owner;
        Name = name;
        NextStatus = nextStatus;
    }

    To reflect these changes, we’ll also have to change the ProjectTask’s UpdateDatebase method:

    public static void UpdateDatabase(Session session)
    {
        TaskStatusSet statusSet = FindTaskStatusSet(session);
        if (statusSet == null)
        {
            string[] statusNames = Enum.GetNames(typeof(ProjectTaskStatuses));
            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();
        }
    }

    And, being good programmers, we’ll provide a test for this functionality:

    [Test]
    public void UpdateDatabase_CorrectStatusSequence()
    {
        TaskStatusSet statusSet = ProjectTask.FindTaskStatusSet(Session);
        Assert.IsNotNull(statusSet);
    
        TaskStatus currentStatus = statusSet.FirstStatus;
        Assert.AreEqual(ProjectTaskStatuses.Draft.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatuses.NotStarted.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatuses.InProgress.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatuses.Paused.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatuses.Completed.ToString(), currentStatus.Name);
    
        Assert.AreEqual(null, currentStatus.NextStatus);
    }

    We also need to write a functional test, in EasyTest, to ensure that users can’t set an incorrect status:

    #DropDB XProjectEasyTest
    
    #Application XProjectWin
    #Application XProjectWeb
    
    *Action Navigation(Project Tasks)
    *Action New
    
    *CheckFieldValues
     Status = Draft
     
    !FillForm
     Status = Completed

    When we run this test it fails, of course, as we’ve not yet specified how we are going to limit the user’s choice of Status. Now, there are two ways of going about this:

    1. We can limit the available Statuses in the combobox.
    2. We can make the combobox read only and apply the Status via an Action which would hold all the required logic for specifying the legal next status.

    I think we’ll favour the second option as the cleaner solution, so let’s mark the ProjectTask’s Status property with the Custom attribute, to make it read-only:

    [DataSourceProperty("StatusSet.Items"), Custom("AllowEdit", "false")]
    public TaskStatus Status {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        protected set { SetPropertyValue<TaskStatus>("Status", value); }
    }

    This makes the Status field read-only on the UI, but we still have to implement the status changing functionality. To do this we’ll implement a new View Controller:

    public class ChangeTaskStatusController : ViewController
    {
        SimpleAction changeTaskStatusAction;
        public ChangeTaskStatusController()
        {
            changeTaskStatusAction = new SimpleAction(this, "ChangeStatus", PredefinedCategory.RecordEdit);
            changeTaskStatusAction.Execute += new SimpleActionExecuteEventHandler(action_Execute);
            changeTaskStatusAction.SelectionDependencyType = SelectionDependencyType.RequireSingleObject;
            TargetObjectType = typeof(ProjectTask);
        }
    private void action_Execute(object sender, SimpleActionExecuteEventArgs e)
    {
        ((ProjectTask)e.CurrentObject).Status = ((ProjectTask)e.CurrentObject).Status.NextStatus;
    }
    }

    As you can see from the constructor declaration, we didn’t use the Controller Designer. Instead we subscribe to the events and create an Action manually, adding the following functionality to our application:

    2

     1

    Well okay, this works but putting the business logic in the controller is not best practice as it’s hard to test and you get no reuse from your code, it also breaks the “separation of concerns rule”. So, to fix this, we’ll add the SetNextStatus method to the ProjectTask class:

    public void SetNextStatus()
    {
        if (this.Status == this.StatusSet.LastStatus)
        {
            throw new InvalidOperationException();
        }
        this.Status = this.Status.NextStatus;
    }

    And we’ll modify the controller’s execute event handler:

    private void action_Execute(object sender, SimpleActionExecuteEventArgs e)
    {
        ProjectTask currentTask = (ProjectTask)e.CurrentObject;
    
        currentTask.SetNextStatus();
        if (currentTask.Status == currentTask.StatusSet.LastStatus)
        {
            changeTaskStatusAction.Enabled.SetItemValue(string.Empty, false);
        }
    }

    And, just for good measure, we’ll write a test to ensure that an exception is thrown if a wrong status is set:

    [Test]
    public void IncorrectStatusSet()
    {
        ProjectTask task = new ProjectTask(Session);
    
        for (int i = 0; i < task.StatusSet.Items.Count - 1; i++)
        {
            task.SetNextStatus();
        }
    
        try
        {
            task.SetNextStatus();
            Assert.Fail();
        }
        catch (InvalidOperationException) { }
    }

    Since a task’s status is now changed by a dedicated method, we’ll make the ProjectTask’s Status field read-only, by making the setter protected, clearing all the old attributes and applying the Persistent attribute:

    [Persistent]
    public TaskStatus Status {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        protected set { SetPropertyValue<TaskStatus>("Status", value); }
    }

    To ensure that the ChangeStatus Action is activated/deactivated correctly, we’ll write a functional test. The test requires a couple of test ProjectTasks in the test database. So, we’ll extend the Updater’s UpdateDatabaseAfterUpdateSchema method of the Xproject.Module project:

    public class Updater : ModuleUpdater {
       ...
        public override void UpdateDatabaseAfterUpdateSchema() {
            base.UpdateDatabaseAfterUpdateSchema();
    
            ProjectTask.UpdateDatabase(Session);
    
    #if EASYTEST
            ProjectTask task = new ProjectTask(Session);
            task.Name = "DraftTask";
            task.Save();
    
            task = new ProjectTask(Session);
            task.Name = "CompletedTask";
            task.SetNextStatus();
            task.SetNextStatus();
            task.SetNextStatus();
            task.SetNextStatus();
            task.Save();
    #endif
    
        }
    }

    And here’s the EasyTest test:

    #DropDB XProjectEasyTest
    
    #Application XProjectWin
    #Application XProjectWeb
    
    *Action Navigation(Project Tasks)
    
    *Action Filter(All Tasks)
    
    *SelectRecords
     Columns = Name
     Row = DraftTask
     
    *ActionAvailable Change Status
    
    *ClearSelection
    
    *SelectRecords
     Columns = Name
     Row = CompletedTask
     
    !ActionAvailable Change Status
    
    *ClearSelection
    
    *SelectRecords
     Columns = Name
     Row = DraftTask
     
    *ActionAvailable Change Status

    Umm… Which doesn’t work. If you run it, it fails with: “Error in test: Action: 'Change Status' is available, line 22”. It looks like, once disabled, the button stays disabled even for other project tasks. To correct this, we’ll modify the controller – update the action_Execute event handler and add another couple of event handler to update the Action’s status based on the currently selected task:

    public class ChangeTaskStatusController : ViewController {
        …
        protected override void OnActivated() {
            base.OnActivated();
    
            View.CurrentObjectChanged += new EventHandler(View_CurrentObjectChanged);
        }
        void View_CurrentObjectChanged(object sender, EventArgs e) {
            UpdateChangeStatusActionState();
        }
        private void UpdateChangeStatusActionState() {
            if(View.CurrentObject != null)
                changeTaskStatusAction.Enabled["ChangeTaskStatusController"] = ((ProjectTask)View.CurrentObject).CanChangeStatus;
        }
        private void action_Execute(object sender, SimpleActionExecuteEventArgs e) {
            ProjectTask currentTask = (ProjectTask)e.CurrentObject;
            currentTask.SetNextStatus();
            UpdateChangeStatusActionState();
        }
    
    }

    For this updated Controller to work, we’ll add a new CanChangeStatus property to the ProjectTask class:

    [Browsable(false)]
    public bool CanChangeStatus
    {
        get { return this.Status != this.StatusSet.LastStatus; }
    }

    Now, if we run the test again we see that it passes – Phew!

    Well, I think we’ll leave it there for this post, the code is available if you want to play along at home. In the next post we’ll add a new kind of task with a new set of statuses, I hope you’ll join me for that. Until then, happy XAFing!

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.