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:
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:
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.
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>
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>
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:
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!