Blogs

Gary's Blog

XAF – Project Management Application #5

     

Welcome along to this the fifth post in the series. In this blog post we are going to extend our application so that we can track the amount of time we spend on each project and also enable our users to visualize that time with some kind of progress bar. Before we do that though, we’re going to fix a little problem we introduced earlier. Remember when we were adding validation to our application and we decided that a ProjectTask’s start date couldn’t before today and we gave that validation the “save” context? Well that seemed sensible enough, I mean a task can’t have started before today, right? Well, it turns out we were a little too clever for our own good, ‘cos you see we don’t only save a task when we first create it do we? No, we also save it when we edit it, and our validation stops us from saving edited tasks which, let’s face it, isn’t really optimal is it? So, for now, we’ll just remove the validation altogether and we’ll look at fixing this “bug” later. To do this, simply remove this line from the code:

[RuleCriteria("ProjectTask-StartDateMustBeAfterToday", DefaultContexts.Save, "StartDate >= '@CurrentDate'  ")]

The next thing we are going to do is to enable our users to format the descriptions for Projects and ProjectTasks. To do this we are going to use the HMTL property editors supplied with XAF by adding the HTMLPropertyEditorWindowsFormsModule to the Windows Forms application project and HTMLPropertyEditorAspNetModule to the ASP.NET Web application project. Then we’ll invoke the Module Editor for the Windows Forms Module Project and ASP.NET Web Module Project, and set the corresponding HTML Property Editors for the Project and ProjectTask’s description properties. The resulting code from the XAFML files are provided here for your information. First the Winforms differences file:

<?xml version="1.0" ?>
<Application>
    <BOModel>
        <Class Name="XProject.Module.Entities.Project">
            <Member Name="Description" PropertyEditorType="DevExpress.ExpressApp.HtmlPropertyEditor.Win.HtmlPropertyEditor" />
        </Class>
        <Class Name="XProject.Module.Entities.ProjectTask">
            <Member Name="Description" PropertyEditorType="DevExpress.ExpressApp.HtmlPropertyEditor.Win.HtmlPropertyEditor" />
        </Class>
    </BOModel>
</Application>

And the Webforms differences file:

<?xml version="1.0"?>
<Application>
    <BOModel>
        <Class Name="XProject.Module.Entities.Project">
            <Member Name="Description" PropertyEditorType="DevExpress.ExpressApp.HtmlPropertyEditor.Web.ASPxHtmlPropertyEditor" />
        </Class>
        <Class Name="XProject.Module.Entities.ProjectTask">
            <Member Name="Description" PropertyEditorType="DevExpress.ExpressApp.HtmlPropertyEditor.Web.ASPxHtmlPropertyEditor" />
        </Class>
    </BOModel>
</Application>

Note that as we specify the property editors in the module projects, and not the application projects, you won’t be able to select the HTML Property Editors in the Model Editor’s dropdowns for the Description fields, instead you’ll have to manually enter the fully-qualified names of the property editors.

The change that makes to our applications is shown below:

1  2

Okay, let’s do some more cosmetic stuff. I don’t know about you but I’m fed up looking at the default images in the view and in the Navigation Bar, so let’s set about customizing them shall we? We’ll use a couple of images from the standard images library supplied with XAF. We’ll set the BO_Folder image for the Project business class and the BO_Task image for the ProjectTask business class, via the Model Editor invoked for the Module Project:

<?xml version="1.0" ?>
<Application Title="XProject" Logo="ExpressAppLogo" >
    <BOModel>
        <Class Name="XProject.Module.Entities.Project" ImageName="BO_Folder">
        </Class>
        <Class Name="XProject.Module.Entities.ProjectTask" ImageName="BO_Task">
        </Class>

...

</Application>

Having made this change our application looks much better:

3  4

That done, let’s turn our attention back to the main point of this post which is to track, calculate and visualize the time spent working on a Project. The first thing we are going to do is to add four properties: PlannedHours, RemainingHours, HoursSpent, and TotalHours to our ProjectTask class. We’ll also introduce the TaskWork class and declare an association between this class and the ProjectTask:

public class ProjectTask : BaseObject {

...

public float PlannedHours {
            get { return GetPropertyValue<float>("PlannedHours"); }
            set { 
                SetPropertyValue<float>("PlannedHours", value);
                if (!IsLoading) {
                    RemainingHours = value - HoursSpent;
                }
            }
        }
        public float RemainingHours {
            get { return GetPropertyValue<float>("RemainingHours"); }
            set { SetPropertyValue<float>("RemainingHours", value); }
        }

        [PersistentAlias("TaskWork[].Sum(HoursSpent)")]
        public float HoursSpent {
            get { return (float)(EvaluateAlias("HoursSpent") ?? 0f); }
        }
        public float TotalHours {
            get { return RemainingHours + HoursSpent; }
        }
        [Association, Aggregated]
        public XPCollection<TaskWork> TaskWork {
            get { return GetCollection<TaskWork>("TaskWork"); }
        }
}


public class TaskWork : BaseObject {
        public TaskWork(Session session)
            : base(session) { }

        public override void AfterConstruction() {
            base.AfterConstruction();
            Date = DateTime.Now;
        }
        [Association]
        public ProjectTask Task {
            get { return GetPropertyValue<ProjectTask>("Task"); }
            set { 
                SetPropertyValue<ProjectTask>("Task", value);
                if (!IsLoading && value != null && DoneBy == null) {
                    DoneBy = Task.AssignedTo;
                }
            }
        }
        public DateTime Date {
            get { return GetPropertyValue<DateTime>("Date"); }
            set { SetPropertyValue<DateTime>("Date", value); }
        }
        public Employee DoneBy {
            get { return GetPropertyValue<Employee>("DoneBy"); }
            set { SetPropertyValue<Employee>("DoneBy", value); }
        }
        public float HoursSpent {
            get { return GetPropertyValue<float>("HoursSpent"); }
            set { SetPropertyValue<float>("HoursSpent", value); }
        }
        public string WorkSummary {
            get { return GetPropertyValue<string>("WorkSummary"); }
            set { SetPropertyValue<string>("WorkSummary", value); }
        }
    }

Note the use of the PersistentAlias attribute. This attribute tells XAF that the property is not persistent but instead is calculated from the provided persistent field or fields. In this case XAF is being told that when we want to know the value of the HoursSpent property of the ProjectTask, then it should be calculated by summing the HoursSpent properties of the associated TaskWork objects.

5  6

Let’s ensure that the the TaskWork list view has a new item row, via the Model Editor invoked for the Module Project. Whilst we are there, let’s set the date format too:

<?xml version="1.0" ?>
<Application Title="XProject" Logo="ExpressAppLogo" >
    <BOModel>
        <Class Name="XProject.Module.Entities.TaskWork" DefaultListViewAllowEdit="True" DefaultListViewNewItemRowPosition="Top">
            <Member Name="Date" DisplayFormat="{0:g}" />
        </Class>
      ...
      </BOModel>
...
</Application>
7  8 

Having done that, we’ll change the notifications to update the HoursSpent value in the details view:

public class ProjectTask : BaseObject {

...

        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;
        }
        void col_CollectionChanged(object sender, XPCollectionChangedEventArgs e) {
            if (e.CollectionChangedType == XPCollectionChangedType.BeforeRemove) {
                RemainingHours += ((TaskWork)e.ChangedObject).HoursSpent;
                UpdateHoursSpent();
            }
        }
        internal void UpdateHoursSpent() {
            OnChanged("HoursSpent");
        }
    }
public class TaskWork : BaseObject {

    ...

        public float HoursSpent {
            get { return GetPropertyValue<float>("HoursSpent"); }
            set { 
                float oldValue = HoursSpent;
                SetPropertyValue<float>("HoursSpent", value);
                if (!IsLoading && Task != null) {
                    float difference = value - oldValue;
                    Task.RemainingHours -= difference;
                    Task.UpdateHoursSpent();
                }
            }
    }

Being good developers we’ll now create a test to ensure our hours calculation is correct. We’ll use NUnit for this so you’ll need to have that installed if you are playing along at home:

public abstract class BaseXpoTest {
    private UnitOfWork session;
    [SetUp]
    public void SetUp() {
        DevExpress.Xpo.Session.DefaultSession.Disconnect();
        XpoDefault.DataLayer = new SimpleDataLayer(new InMemoryDataStore(new DataSet(), AutoCreateOption.SchemaOnly));
        session = new UnitOfWork();
    }
    public UnitOfWork Session {
        get { return session; }
    }
}
[TestFixture]
public class ProjectTests : BaseXpoTest {
    [Test]
    public void TaskHoursCalculation() {
        ProjectTask task = new ProjectTask(Session);
        task.Name = "test";
        task.PlannedHours = 10;
        Assert.AreEqual(10, task.RemainingHours);
        Assert.AreEqual(10, task.TotalHours);
        Assert.AreEqual(0, task.Progress);

        task.RemainingHours = 15;
        Assert.AreEqual(15, task.TotalHours);
        Assert.AreEqual(0, task.Progress);

        TaskWork taskWork1 = new TaskWork(Session);
        taskWork1.Task = task;
        taskWork1.HoursSpent = 5;            
        Assert.AreEqual(10, task.PlannedHours);
        Assert.AreEqual(5, task.HoursSpent);
        Assert.AreEqual(10, task.RemainingHours);
        Assert.AreEqual(15, task.TotalHours);
        Assert.AreEqual(5f / 15f, task.Progress);

        taskWork1.HoursSpent = 10;
        Assert.AreEqual(10, task.PlannedHours);
        Assert.AreEqual(10, task.HoursSpent);
        Assert.AreEqual(5, task.RemainingHours);
        Assert.AreEqual(15, task.TotalHours);
        Assert.AreEqual(10f / 15f, task.Progress);

        taskWork1.Delete();
        Assert.AreEqual(15, task.RemainingHours);
    }
    [Test]
    public void TaskWorkInitialization() {
        Employee emp = new Employee(Session);
        emp.UserName = "test emp";
        ProjectTask task = new ProjectTask(Session);
        task.Name = "test";
        task.AssignedTo = emp;
        TaskWork taskWork1 = new TaskWork(Session);
        taskWork1.Task = task;
        Assert.AreEqual(emp, taskWork1.DoneBy);
        int msdiff = (DateTime.Now - taskWork1.Date).Milliseconds;
        Assert.IsTrue(msdiff < 500);
    }
}

To show the progress of a ProjectTask we’ll create two property editors, one for Winforms and one for Webforms. Firstly, Winforms. Start by creating a custom progress bar:

public class TaskProgressBarControl : ProgressBarControl {
    static TaskProgressBarControl() {
        RepositoryItemTaskProgressBarControl.Register();
    }
    public override string EditorTypeName { get { return RepositoryItemTaskProgressBarControl.EditorName; } }
    protected override object ConvertCheckValue(object val) {
        return val;
    }
}
public class RepositoryItemTaskProgressBarControl : RepositoryItemProgressBar {
    protected internal const string EditorName = "TaskProgressBarControl";
    protected internal static void Register() {
        if (!EditorRegistrationInfo.Default.Editors.Contains(EditorName)) {
            EditorRegistrationInfo.Default.Editors.Add(new EditorClassInfo(EditorName, typeof(TaskProgressBarControl),
                typeof(RepositoryItemTaskProgressBarControl), typeof(ProgressBarViewInfo),
                new ProgressBarPainter(), true, EditImageIndexes.ProgressBarControl, 
typeof(DevExpress.Accessibility.ProgressBarAccessible))); } } static RepositoryItemTaskProgressBarControl() { Register(); } protected override int ConvertValue(object val) { try { float number = Convert.ToSingle(val); return (int)(Minimum + number * Maximum); } catch { } return Minimum; } public override string EditorTypeName { get { return EditorName; } } }

Then we can use it in a custom property editor:

[PropertyEditor(typeof(float))]
    public class WinProgressEdit : DXPropertyEditor {
        public WinProgressEdit(Type objectType, DictionaryNode info) : base(objectType, info) { }
        protected override object CreateControlCore() {
            return new TaskProgressBarControl();
        }
        protected override RepositoryItem CreateRepositoryItem() {
            return new RepositoryItemTaskProgressBarControl();
        }
        protected override void SetupRepositoryItem(RepositoryItem item) {
            RepositoryItemTaskProgressBarControl repositoryItem = (RepositoryItemTaskProgressBarControl)item;
            repositoryItem.Maximum = 100;
            repositoryItem.Minimum = 0;
            base.SetupRepositoryItem(item);
        }
    }

Finally, register this property editor for the TaskProject.Progress property via the Model Editor invoked for the Windows Forms module project:

<?xml version="1.0" ?>
<Application>
    <BOModel>
         ...
        <Class Name="XProject.Module.Entities.ProjectTask">
              ...
            <Member Name="Progress" PropertyEditorType="XProject.Module.Win.WinProgressEdit" />
        </Class>
    </BOModel>
</Application>

Now repeat this process for webform:

[PropertyEditor(typeof(float))]
    public class WebProgressEdit : ASPxPropertyEditor {
        public WebProgressEdit(Type objectType, DictionaryNode info) : base(objectType, info) { }

        private void SetProgressValue() {
            TaskProgressBar progressBar = InplaceViewModeEditor as TaskProgressBar;
            if (progressBar == null) progressBar = Editor as TaskProgressBar;
            if (progressBar != null) {
                progressBar.ProgressValue = PropertyValue;
            }
        }
        protected override WebControl CreateEditModeControlCore() {
            TaskProgressBar result = new TaskProgressBar();
            result.Width = Unit.Percentage(100);
            return result;
        }
        protected override WebControl CreateViewModeControlCore() {
            return CreateEditModeControlCore();
        }
        protected override void ReadViewModeValueCore() {
            base.ReadViewModeValueCore();
            SetProgressValue();
        }
        protected override void ReadEditModeValueCore() {
            base.ReadEditModeValueCore();
            SetProgressValue();
        }
    }

    public class TaskProgressBar : ASPxProgressBar {
        private float progressValue = 0;
        public object ProgressValue {
            get { return progressValue; }
            set {
                progressValue = (float)value;
                Value = Minimum + Maximum * progressValue;
            }
        }
    }

And register this property editor for the TaskProject.Progress property via the Model Editor invoked for the ASP.NET Web module project:

<?xml version="1.0"?>

<Application>
  <BOModel>
  ...
    <Class Name="XProject.Module.Entities.ProjectTask">
     ...
      <Member Name="Progress" PropertyEditorType="XProject.Module.Web.WebProgressEdit" />
    </Class>
  </BOModel>

</Application>
9  10 

As you can see from the above image, our progress bar isn’t skinned properly in ASP.Net, it displays in a grey colour and not the bluish of the rest of the theme, let’s fix that shall we? To do that we need to add the following section to the XProjectStage3.Web\App_Themes\XafDefault\Editors\styles.css file:

/* -- ProgressBar -- */
.dxeProgressBar_xaf, .dxeProgressBar_xaf td
{
    font-family: Tahoma, Verdana, Arial;
    font-size: 9pt;
       color: Black;
}
.dxeProgressBar_xaf .dxePBMainCell, .dxeProgressBar_xaf td
{
    padding: 0px;
}
.dxeProgressBar_xaf
{
    border: Solid 1px #A3C0E8;
    background-image: url('edtProgressBack.gif');
    background-repeat: repeat-x;
    background-color: #f0f0f0;
}
.dxeProgressBarIndicator_xaf
{
    background-color: #deedff;
    background-image: url('edtProgressIndicatorBack.gif');
    background-repeat: repeat-x;
}

Then all we have to do is to create a skin file (provided with the code):

<%@ Register TagPrefix="dx" Namespace="XProject.Module.Web" Assembly="XProject.Module.Web" %>
<dx:TaskProgressBar runat="server" Height="25px" CssFilePath="~/App_Themes/XafDefault/{0}/styles.css" CssPostfix="xaf">
</dx:TaskProgressBar>

And everything will look so much nicer:

11

Right, I think that is quite enough for today, don’t you? As always, if you are playing along at home, you can access the code from here. Until next time… happy XAFing!

Published Aug 18 2009, 05:03 PM by Gary Short (DevExpress)
Filed under:
Technorati tags: XAF
Bookmark and Share

Comments

 

XAF ??? Project Management Application Index - Gary's Blog said:

Pingback from  XAF ??? Project Management Application Index - Gary's Blog

August 18, 2009 12:10 PM
 

Luc DEBRUN said:

Ok .. now we are talking. This series is starting to get interesting. Good work. Please continue.

Luc

August 18, 2009 9:03 PM
 

Steve Sharkey said:

PersistantAlias... How did I miss that? I've been using readonly properties to achieve the same thing!

August 19, 2009 2:54 AM
 

Gary Short (DevExpress) said:

Every day's a school day Steve! :-)

August 19, 2009 5:21 AM
 

FredrikE said:

Is it possible to have changes to Planned Hours, Remaining Hours, Total Hours and Progress reflected in the web UI, similarly to the win forms UI, i.e making them "update each other" (client-side) without having to validate or save the view?

August 19, 2009 6:15 AM
 

Chloe Anfield said:

That PersistentAlias usage is new to me as well. Good work Gary.

August 19, 2009 6:33 AM
 

DiEm said:

The full source code is available to download ?

Thank you,

Dimitris

August 19, 2009 12:30 PM
 

Valuable Internet Information » XAF ??? Project Management Application #5 - Gary's Blog said:

Pingback from  Valuable Internet Information » XAF ??? Project Management Application #5 - Gary's Blog

August 19, 2009 8:05 PM
 

Gary Short (DevExpress) said:

@DiEm, yes it is, from the link I provided in the last sentence of my post.

August 20, 2009 5:26 AM
 

Huseyn Guliyev said:

Hi, i believe this is more correct:

 protected override int ConvertValue(object val) {

       try {

           float number = Convert.ToSingle(val);

           return (int)(Minimum + number * (Maximum-Minimum));

       }

       catch { }

       return Minimum;

   }

See,  (Maximum-Minimum).

Thanks for nice tutorial.

Huseyn

September 10, 2009 8:28 AM
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 8:30am and 5:00pm 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.