XCRM: Activities

XAF Team Blog
09 October 2008

EDIT:
Domain Components (DC) is in maintenance mode and we do not recommend its use in new software projects.

This post is one in a series about our work on Domain Components (DC) framework and related component libraries. I’m just describing what we are working on, what problems we are facing and what results we've got so far.

This post is entirely devoted to Activities. It's more interesting to design Activities, because they are more complex than other components that I described earlier. There are three points I consider when designing Activities:

  1. There can be many Activity types: a phone call, email, meeting/appointment, task, campaign activity or case/service activity.
  2. An Activity has an owner - a person who is assigned to perform the Activity. The person should be able to see all his/her activities in one list.
  3. An Activity can be related to a Contact, Account, Lead, Opportunity, MarketingCampaign or any other “main” entity record in the system.

Let's go through all these points:

1. We can divide all Activities into two basic categories:

  • Event

An Activity that has clear start and end points. Generally, Events can be scheduled on a calendar. So, the system should be able to show all Events in one Calendar view.

  • Task

A less time-bound Activity than the Event. It may have a DueDate only.

Simple CRM systems work with Tasks and Events simultaneously. More complex systems distinguish these Activity types, so they can track specialized information, like a phone number for a call.

In our application, let’s define a Task and Event. In addition, let's create specialized Activities: a Phone Call, as a variation of the Task Activity, and an Appointment, as a variation of the Event Activity. Here are the tests that express these requirements:

[Test]
public void ActivityTypesTest() {
    ICall call = ObjectSpace.CreateObject<ICall>();
    IAppointment appointment = ObjectSpace.CreateObject<IAppointment>();
    ITask task = ObjectSpace.CreateObject<ITask>();
    IEvent eventObj = ObjectSpace.CreateObject<IEvent>();

    IList<IActivity> allActivities = ObjectSpace.GetObjects<IActivity>(null);
    Assert.AreEqual(4, allActivities.Count);
    IList<IEvent> allEvents = ObjectSpace.GetObjects<IEvent>(null);
    Assert.AreEqual(2, allEvents.Count);
}
[DomainComponent]
public interface IActivity {
    string Subject { get; set; }
    
    IActivityTarget RelatedTo { get; set; }
}
[DomainComponent]
public interface IEvent : IActivity {
    DateTime StartTime { get; set; }
    DateTime EndTime { get; set; }
    bool AllDayEvent { get; set; }
}
public enum TaskPriority { Low, Medium, High }

[DomainComponent]
public interface ITask : IActivity {
    TaskPriority Priority { get; set; }
    DateTime DueDate { get; set; }
}
[DomainComponent]
public interface ICall : ITask {
}
[DomainComponent]
public interface IAppointment : IEvent {
}

I don’t call the RegisterDC method in this test, because I've moved all Activity registrations to the SetUp method (it is called before a test is started by NUnit):

[SetUp]
public override void SetUp() {
    base.SetUp();
    RegisterDC<ILead>();
    RegisterDC<ICampaign>();
    RegisterDC<IContact>();
    RegisterDC<IAccount>();
    RegisterDC<IOpportunity>();
    RegisterDC<IEvent>();
    RegisterDC<IAppointment>();
    RegisterDC<ITask>();
    RegisterDC<ICall>();
    Generate();
}

Note that I don't register the IActivity. This is a base abstract interface that cannot be instantiated in our system.

2. An Activity has an owner – usually the system's user. The owner may have a list of Activities:

[DomainComponent]
public interface ITestUser : IActivityOwner {
}
[TestFixture]
public class ActivityTests : BaseTest {
    …
    [Test]
    public void ActivityOwnershipTest() {
        ITask blogAboutDC = ObjectSpace.CreateObject<ITask>();
        ITestUser romanEremin = ObjectSpace.CreateObject<ITestUser>();
        blogAboutDC.Owner = romanEremin;
        Assert.AreEqual(blogAboutDC, romanEremin.Activities[0]);
    }
}
[DomainComponent]
public interface IActivityOwner {
    IList<IActivity> Activities { get; }
}
[DomainComponent]
public interface IActivity {
    
    IActivityOwner Owner { get; set; }
}

3. An Activity can be related to a Contact, Account and any other “main” record in the system. Some systems allow you to link Activities to several records of different type, but I guess this is just a workaround to make programming easier. Our DC framework should be flexible, so I will design it in “the right way”:

[Test]
public void ActivityRelationTest() {
    IAccount account = ObjectSpace.CreateObject<IAccount>();
    IContact contact = ObjectSpace.CreateObject<IContact>();
    ILead lead = ObjectSpace.CreateObject<ILead>();
    IOpportunity opportunity = ObjectSpace.CreateObject<IOpportunity>();
    ITask task = ObjectSpace.CreateObject<ITask>();

    task.RelatedTo = account as IActivityTarget;
    Assert.IsNotNull(account.Activities);
    Assert.AreEqual(task, account.Activities[0]);
    task.RelatedTo = contact as IActivityTarget;
    Assert.AreEqual(task, contact.Activities[0]);
    task.RelatedTo = lead as IActivityTarget;
    Assert.AreEqual(task, lead.Activities[0]);
    task.RelatedTo = opportunity as IActivityTarget;
    Assert.AreEqual(task, opportunity.Activities[0]);
}

To make this test pass, I should define the IActivityTarget interface. Only the components that implement this interface will be able to be added to the “related to” list:

[DomainComponent]
public interface IActivityTarget {
    IList<IActivity> Activities { get; }
}

[DomainComponent]
public interface IActivity {
    
    IActivityTarget RelatedTo { get; set; }
}

Note that I have not declared a requirement that I want to be able to see all Activity Targets in one list. In that instance, we would have to create a common table for them somehow, and this might be a major restriction on possible use cases of Activities.

I don’t require all IActivityTarget implementers to be in one list, but I still need to be able to edit the RelatedTo field in a UI. This editor should let end-users locate an object of one of predefined types. CRM systems tend to achieve this by adding a combo box with object types. In this instance, end-users first select the type, and then the object of this type:

dc5-01

As far as I know,  we don’t have an editor of this kind yet. So, I've written down a task to implement this editor and added a test that describes this scenario. To tell you the truth, the XpoBuilder has trouble with this test. Let me explain in more detail.

How to persist a reference to the interface that is implemented in several classes.

Here, we have a case of leaky abstraction, impedance mismatch and that sort of thing. The fact is that a relational database is not mapped to the OO world easily. In our case – how would you manually implement the scenario where an Activity can be related to one of several records, say, Account or Contact? I see two ways (note that we exclude the requirement that the Contact and Account must have the same base).

The first approach - Multiple associations.

Make two associations:

dc5-02

The implementation of the IActivity.RelatedTo property will look like this:

public IActivityTarget RelatedTo {
    get {
        if (Contact != null) return Contact;
        if (Account != null) return Account;
        return null;
    }
    set {
        Contact = null;
        if (value is Contact) {
            Contact = value as Contact;
            Account = null;
            return;
        }
        if (value is Account) {
            Contact = null;
            Account = value as Account;
            return;
        }
    }
}

This code is ugly, but other than that it looks fine – the Contact and Account have a list of Activities. An Activity allows you to access a Contact or Account as IActivityTarget. But let’s make a simple change. Let's add the Active property to the IActivityTarget interface:

public interface IActivityTarget {
    IList<IActivity> Activities { get; }
    bool Active { get; set; }
}

XPO will not be able to apply a criteria “[RelatedTo.Active] = true” to Activities on the server, because the RelatedTo is not an FK field and cannot be used in a simple SQL query.

The second approach - Intermediate table.

Create an intermediate table for the IActivityTarget interface:

 dc5-03

In this instance, the implementation of the IActivityTarget interface in the Account and Contact would redirect all calls to the aggregated ActivityTarget class:

public IList<IActivity> Activities {
    get { return ActivityTarget.Activities; }
}

Adding extra properties to the IActivityTarget interface won’t be a problem:

public interface IActivityTarget {
    IList<IActivity> Activities { get; }
    bool Active { get; set; }
}

... because these properties will be contained in the ActivityTarget class, and the Contact and Account will redirect calls to it:

public bool Active {
    get { return ActivityTarget.Active; }
    set { ActivityTarget.Active = value; }
}

But this approach yields another problem – since we hide an indirection, any Activity-related criteria for, say, a Contact (“Activities.Count > 0”) will not be evaluated on the server, because there is no association between the Contact and Activity. As an alternative, we could translate “Activites.Count” to “ActivityTarget.Activities.Count”. This translation might not be very obvious and would lead to problems.

As you can see, that is something for XPO developers to think about. I’m going to talk to them and consider the approach we should use...

Good news! XPO can use persistent aliases to map the Account’s Activities to its ActivityTarget.Actvities. So, we will use the Intermediate Table approach with aliases, and we might not have problems related to criteria. My Activity components can work as is and I can proceed further while waiting for the XpoBuilder to generate what I want.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
Tags
No Comments

Please login or register to post comments.