eXpress App Framework Team

This Blog

October 2008 - Posts

  • How to implement skin selection via Navigation panel?

     

     This article describes how we introduced skin items into the Navigation panel in the FeatureCenter application.

    The FeatureCenter demo application is installed by the XAF installation and you can review all its sources as well as to see how it works.

    One of the famous features of DXperience controls is their skins, see "Skins Overview" at http://documentation.devexpress.com/#WindowsForms/CustomDocument2399 and "Application Wide Skins" video at http://tv.devexpress.com/SkinsPromo1.movie

    		public ReportsController()
    			: base() {
    			InitializeComponent();
    
    		}

    How to demonstrate skins in XAF? The Navigation panel seems to be the best place to introduce a new group of setting, which will switch skins for a whole application.

    By default, a Windows Forms XAF application has a standard action "Choose Skin", which is placed in the "Tools" menu. However, the "Tools" menu is not a place, which will be expected by a developer in a Demo application and we decided to duplicate its items into the Navigation panel as a new group.

    This is how it will look:

     

    
    

    It is used in XAF to realize additional behavior scenarios via controllers. Which type of controller should we choose for our task – WindowController or ViewController?

    Certainly, we need to create the descendant of the WindowController, 'cause our task doesn't concern exact View, but the whole main Window.

     

    So, we need to create Window controller that will realize expected behavior, and customize it in order to implement our task.

     

     

    Step One. Add new WindowController to our solution.

     

    At first we have to select project, to which we will add new controller, in Solution Explorer, and then add new item to it.

    We can do it via Microsoft Visual Studio main menu “Project\Add new item...” or

    vai project context menu - cick right mouse button on the project with suffix “Module.Win” in our solution and select “Add\NewItem...” in the menu, as shown below:

    After that Microsoft Visual Studio will offer you dialog to select new item type and specify it's name:

    Let's the mane of our new coltroller be “SkinsWindowController” and press Add button.

     

    After that we will have a new file in our project with code shown below:

     

    		using System;
    
    using System.ComponentModel;
    
    using System.Collections.Generic;
    
    using System.Diagnostics;
    
    using System.Text;
    
     
    
    using DevExpress.ExpressApp;
    
    using DevExpress.ExpressApp.Actions;
    
    using DevExpress.Persistent.Base;
    
     
    
    namespace FeatureCenter.Module.Win {
    
    public partial class SkinsWindowController : WindowController {
    
    public SkinsWindowController() {
    
    InitializeComponent();
    
    RegisterActions(components);
    
    }
    
    }
    
    }
    
    

     

     

     

    And all our work will occur inside this file, in the SkinsWindowController class.

     

    Step Two. Customize automatically generated code.

     

    Microsoft Visual Studio automatically generate file SkinsWindowController.Designer.cs, so we can place some visaul components into controller. But we don't need it in our case – and we can delete file SkinsWindowController.Designer.cs from our project, and modify SkinsWindowController constructor this way:

    <code>

    public SkinsWindowController() {

    }

     

    </code>

     

    Our controller will operate main application window only, and we can specify this by setting TargetWindowType property of our controller:

     

    <code>

    public SkinsWindowController() {

    this.TargetWindowType = WindowType.Main;

    }

    </code>

     

    Note. Deleting SkinsWindowController.Designer.cs file from the project deprive us possibility of viewing our controller in design mode. So we should use View Code command to edit file contents.

     

    Step Three. Adding links to skins into Navigation panel.

     

    The best way to adjust navigation panel from our controller is override OnActivated method, as shown in code sample below:

     

    <code>

    public partial class SkinsWindowController : WindowController {

    private ChoiceActionItem skinsGroup = null;

    private const string skinsGroupCaption = "Skins";

    protected override void OnActivated() {

    base.OnActivated();

    skinsGroup = new ChoiceActionItem(skinsGroupCaption, skinsGroupCaption, null);

    NavigationController.ShowNavigationItemAction.Items.Add(skinsGroup);

    foreach(ChoiceActionItem item in ChooseSkinController.ChooseSkinAction.Items) {

    ChoiceActionItem newItem = new ChoiceActionItem(item.Caption, item.Caption, null);

    newItem.ImageName = ChooseSkinController.ChooseSkinAction.ImageName;

    skinsGroup.Items.Add(newItem);

    }

    }

    protected ChooseSkinController ChooseSkinController {

    get { return Frame.GetController<ChooseSkinController>(); }

    }

    protected ShowNavigationItemController NavigationController {

    get { return Frame.GetController<ShowNavigationItemController>(); }

    }

    public SkinsWindowController() {

    this.TargetWindowType = WindowType.Main;

    }

    }

    </code>

     

    In our OnActivated method we called base.OnActivated() first, then done our additions:

    • create new navigation item for new group with name “Skins”:

    <code>

    skinsGroup = new ChoiceActionItem(skinsGroupCaption, skinsGroupCaption, null);

    </code>

    • add this group to navigation panel with the help of the corresponding system controller:

    <code>

    NavigationController.ShowNavigationItemAction.Items.Add(skinsGroup);

    </code>

    • add items corresponding available skins from the chose skin system controller:

    <code>

    foreach(ChoiceActionItem item in ChooseSkinController.ChooseSkinAction.Items) {

    ChoiceActionItem newItem = new ChoiceActionItem(item.Caption, item.Caption, null);

    newItem.ImageName = ChooseSkinController.ChooseSkinAction.ImageName;

    skinsGroup.Items.Add(newItem);

    }

    </code>

     

    As you have noticed, we also write some code:

    • for storing a link to our “Skin” group:

    <code>

    private ChoiceActionItem skinsGroup = null;

    </code>

    • for storing the caption of our group:

    <code>

    private const string skinsGroupCaption = "Skins";

    </code>

    • properties, that simplify for us access to the system controllers:

    <code>

    protected ChooseSkinController ChooseSkinController {

    get { return Frame.GetController<ChooseSkinController>(); }

    }

    protected ShowNavigationItemController NavigationController {

    get { return Frame.GetController<ShowNavigationItemController>(); }

    }

    </code>

     

    And after this step we can run our windows project and see a group “Skins” in the navigation panel, which contains a list of available skins.

     

    But choosing this actions doesn't result on current skin selection. Why?

    That's why we don't link our items in the navigation panel with real ChooseSkin action. And we will do in in the next step.

     

    Step Four. Providing links to ChooseSkin action.

     

    By default, each item in Navigation panel contains a ViewShortcut to corresponding view, and than user select the item, process ViewShortcut, that linked with item.Data property – third parameter of the ChoiceActionItem constructor (that was null at previous step).

     

    So, for each item added to Navigation panel we create a ViewShortcut to SkinDemoObject (simple business object we created to have something else empty space in application window). The ViewShortcut class is a list of [Key, Value] pairs, and we added a reference to the skin item as a new item:

    <code>

    viewShortcut.Add("SkinID", (string)item.Data);

    </code>

     

    As result we have following:

     

    <code>

    public partial class SkinsWindowController : WindowController {

    private ChoiceActionItem skinsGroup = null;

    private const string skinsGroupCaption = "Skins";

    protected override void OnActivated() {

    base.OnActivated();

    skinsGroup = new ChoiceActionItem(skinsGroupCaption, skinsGroupCaption, null);

    NavigationController.ShowNavigationItemAction.Items.Add(skinsGroup);

    foreach(ChoiceActionItem item in ChooseSkinController.ChooseSkinAction.Items) {

    ViewShortcut viewShortcut = new ViewShortcut(typeof(SkinDemoObject), null, Application.GetListViewId(typeof(SkinDemoObject)));

    viewShortcut.Add("SkinID", (string)item.Data);

    ChoiceActionItem newItem = new ChoiceActionItem(item.Caption, item.Caption, viewShortcut);

    newItem.ImageName = ChooseSkinController.ChooseSkinAction.ImageName;

    skinsGroup.Items.Add(newItem);

    }

    }

    protected ChooseSkinController ChooseSkinController {

    get { return Frame.GetController<ChooseSkinController>(); }

    }

    protected ShowNavigationItemController NavigationController {

    get { return Frame.GetController<ShowNavigationItemController>(); }

    }

    public SkinsWindowController() {

    this.TargetWindowType = WindowType.Main;

    }

    }

    </code>

     

    In platform-independent module we added also a simple business object

    <code>

    using System;

    using DevExpress.Persistent.BaseImpl;

    using DevExpress.Xpo;

    using DevExpress.ExpressApp.Demos;

    using DevExpress.Persistent.Base;

     

    namespace FeatureCenter.Module.Skins {

    public class SkinDemoObject : BaseObject {

    private string name;

    public SkinDemoObject(Session session) : base(session) { }

    public string Name {

    get { return name; }

    set { name = value; }

    }

    }

    }

    </code>

     

    Step Five. Executing ChooseSkin action then Navigation panel item selected .

     

    In step four we add an information about skin to Navigation panel items. In order to change skin when user select appropriate item in navigation panel we should process just a pair of events – CustomShowNavigationItem (to execute ChooseSkin action) and CustomUpdateSelectNavigationItem (to say that we proceeed selected Navigation panel item):

     

    <code>

    public partial class SkinsWindowController : WindowController {

    private ChoiceActionItem skinsGroup = null;

    private const string skinsGroupCaption = "Skins";

     

    private void NavigationController_CustomShowNavigationItem(object sender, CustomShowNavigationItemEventArgs e) {

    ViewShortcut shortcut = e.ActionArguments.SelectedChoiceActionItem.Data as ViewShortcut;

    if(shortcut != null) {

    ChoiceActionItem skinItem =

    ChooseSkinController.ChooseSkinAction.Items.Find(shortcut["SkinID"]);

    if(skinItem != null) {

    ChooseSkinController.ChooseSkinAction.DoExecute(skinItem);

    }

    }

    }

     

    private void NavigationController_CustomUpdateSelectNavigationItem(object sender, CustomUpdateSelectedItemEventArgs e) {

    if(e.ProposedSelectedItem != null) {

    if(e.ProposedSelectedItem.ParentItem == skinsGroup) {

    e.Handled = true;

    }

    }

    }

     

    public override void UpdateModel(Dictionary dictionary) {

    base.UpdateModel(dictionary);

    new DefaultSkinListGenerator(dictionary).StorePredefinedLookAndFeelStyle("Black");

    }

     

    protected override void OnActivated() {

    base.OnActivated();

    skinsGroup = new ChoiceActionItem(skinsGroupCaption, skinsGroupCaption, null);

    NavigationController.ShowNavigationItemAction.Items.Add(skinsGroup);

    foreach(ChoiceActionItem item in ChooseSkinController.ChooseSkinAction.Items) {

    ViewShortcut viewShortcut = new ViewShortcut(typeof(SkinDemoObject), null, Application.GetListViewId(typeof(SkinDemoObject)));

    viewShortcut.Add("SkinID", (string)item.Data);

    ChoiceActionItem newItem = new ChoiceActionItem(item.Caption, item.Caption, viewShortcut);

    newItem.ImageName = ChooseSkinController.ChooseSkinAction.ImageName;

    skinsGroup.Items.Add(newItem);

    }

    NavigationController.CustomShowNavigationItem += new

    EventHandler<CustomShowNavigationItemEventArgs>(NavigationController_CustomShowNavigationItem);

    NavigationController.CustomUpdateSelectedItem += new

    EventHandler<CustomUpdateSelectedItemEventArgs>(NavigationController_CustomUpdateSelectNavigationItem);

    }

     

    protected override void OnDeactivating() {

    base.OnDeactivating();

    NavigationController.CustomUpdateSelectedItem -= new EventHandler<CustomUpdateSelectedItemEventArgs>(NavigationController_CustomUpdateSelectNavigationItem);

    NavigationController.CustomShowNavigationItem -= new EventHandler<CustomShowNavigationItemEventArgs>(NavigationController_CustomShowNavigationItem);

    }

     

    protected ChooseSkinController ChooseSkinController {

    get { return Frame.GetController<ChooseSkinController>(); }

    }

     

    protected ShowNavigationItemController NavigationController {

    get { return Frame.GetController<ShowNavigationItemController>(); }

    }

     

    public SkinsWindowController() {

    this.TargetWindowType = WindowType.Main;

    }

     

    }

    </code>

     

    We add appropriate handlers in OnActivated() method and delete them in OnDeactivating() method.

     

    NavigationController_CustomShowNavigationItem handler takes ViewShortcut, associated with selected Navigation panel item, finds in this sortcut link to corresponding skin action item, and executes ChooseSkin action to select choosed skin.

     

    NavigationController_CustomUpdateSelectNavigationItem sets e.Handled to true if selected item is in “skinsGroup”.

     

     

    Step Six. Compiling, executing and testing.

     

    After all we can compile and run our solution, where we can see now new group with available skins in Navigation panel, moreover, when user select one of these items, appearance of our application is changed in accordance to selected skin.

     

     

    Note. This approach can be used for adding links to another actions into Navigation panel.

  • DC: Combining associations

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    Remember that the base idea of creating the Domain Component Technology is to be able to compose an application from reusable blocks – components. These blocks can be bought from third-party sources. This allows you to reuse well-polished blocks. But this also means that you cannot change the sources of these blocks. The other day, while working on XCRM, I tried to combine two component sets, but encountered a problem. In this post, I want to discuss this problem in detail.

    Imagine that you have two domain component sets – Tasks and Ownership Security. Both of them contain DC interfaces, domain logic, Controllers, Actions, UI settings and end-user documentation. You want to reuse everything. Now, I will focus on DC interfaces only. Here are the interfaces that are related to Tasks:

    [DomainComponent]
    public interface ITask {
        string Subject { get; set; }
        ITaskOwner AssignedTo { get; set; }
    }
    [DomainComponent]
    public interface ITaskOwner {
        IList<ITask> Tasks { get; }
    }

    The developer, who designed this component, made it independent from the User concept. A Task can be assigned to any object that implements the ITaskOwner.

    Here are the interfaces that define Owership Security:

    [DomainComponent]
    public interface ISecuredItem {
        ISecurityOwner Owner { get; set; }
    }
    [DomainComponent]
    public interface ISecurityOwner {
        bool IsManager { get; set; }
    }

    Here, a user can only see his/her own items, if he/she has the IsManager property set to false. The developer, who designed this component, made it independent from other components again.

    Now, you want to assemble your own system out of these components. You want to have Tasks secured using Ownership Security. So, you declare combined domain components and register them as entities in the following manner:

    [DomainComponent]
    public interface IUser : ITaskOwner, ISecurityOwner {
    }
    
    [DomainComponent]
    public interface ISecuredTask : ITask, ISecuredItem {
    }
    [Test]
    public void TestOwner() {
        RegisterDC<IUser>();
        RegisterDC<ISecuredTask>();
        IUser user = CreateObject<IUser>();
        ISecuredTask task = CreateObject<ISecuredTask>();
        task.Owner = user;
    }

    What will be generated from these components? The answer is quite obvious. The IUser and ISecuredTask interfaces will be implemented and mapped to the following tables:

    dc06-1

    This structure allows you to assign different Users to a Task object's AssignedTo and Owner properties. But, what if you want these properties always set to the same User? For this purpose, I could provide additional logic that would synchronize both these properties. However, I would still have two associations in the database, which I don’t need. I want to have a table structure like this:

    dc06-2

    I need a way to tell the XpoBuilder that I want the ITask.AssignedTo and ISecuredItem.Owner to be mapped to the same column/property. Remember, I cannot change the source code of the Tasks and Ownership Security components. I can only change my IUser and ISecuredTask. So, to implement the ISecuredTask in accordance to my requirement, I can write something like this:

    public class SecuredTask : XPObject, ISecuredTask  {
        public User Owner  
        ITaskOwner ITask.AssignedTo {
            get { return (ITaskOwner)Owner; }
            set { Owner = (User)value; }
        }
        ISecurityOwner ISecuredItem.Owner {
            get { return (ISecurityOwner)Owner; }
            set { Owner = (User)value; }
        }
    }

    Here, I build a new association and explicitly implement both the ITask.AssignedTo and ISecuredItem.Owner properties.

    It'd be much easier, if I could tell the XpoBuilder something like this:

    [DomainComponent]
    public interface ISecuredTask : ITask, ISecuredItem {
        [Implements("ITask.AssignedTo", "ISecuredItem.Owner")]
        new IUser Owner { get; set; }
    }

    This would indicate that I have a new association between a SecuredTask and User, and I want the ITask.AssignedTo and ISecuredItem.Owner properties to be “mapped'” to it. But something tells me that this would break associations. For instance, when I try to get the User.Tasks, XPO won’t be able to find a back reference (it expects the AssignedTo property, but it is non-persistent). So again, I had to beg the XPO team for some alias magic.

    Please, XPO team, make my (our?) dream come true!

  • XCRM: Activities

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    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.

  • XCRM: Marketing Campaigns and Customer support

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    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.

    Marketing Campaigns

    A Marketing Campaign has no relationships. However, a Lead and Opportunity may reference a Campaign:

    [Test]
    public void CampaignRelationships() {
        RegisterDC<ILead>();
        RegisterDC<ICampaign>();
        RegisterDC<IContact>();
        RegisterDC<IAccount>();
        RegisterDC<IOpportunity>();
        Generate();
        ICampaign campaign = ObjectSpace.CreateObject<ICampaign>();
        campaign.Name = "Playboy back cover";
        ILead lead = ObjectSpace.CreateObject<ILead>();
        IOpportunity opportunity = ObjectSpace.CreateObject<IOpportunity>();
        lead.Campaign = campaign;
        opportunity.Campaign = campaign;
    }
    [DomainComponent]
    public interface ICampaign {
        string Name { get; set; }
    }
    [DomainComponent]
    public interface ILead {
        
        ICampaign Campaign { get; set; }
    }
    [DomainComponent]
    public interface IOpportunity {
        
        ICampaign Campaign { get; set; }
    }

    Instead of adding the Campaign reference property to both the ILead and IOpportunity components, I could add the ICampaignResult interface and inherit the ILead and IOpportunity from it. But I think, this is overkill. However, if one more class references the Campaign, and business logic is supplied for this reference, I'll choose this approach.

    Cases (customer support incidents)

    A Case is a record about a user's inquiry or problem. It is linked to an Account and/or a Contact. If the Contact is set, the Account is initialized from the Contact’s Account, if any:

    [Test]
    public void CaseRelationships() {
        RegisterDC<IContact>();
        RegisterDC<IAccount>();
        RegisterDC<ICase>();
        RegisterDC<IOpportunity>();
        Generate();
        IContact roman = ObjectSpace.CreateObject<IContact>();
        roman.FirstName = "Roman";
        roman.LastName = "Eremin";
        IAccount dx = ObjectSpace.CreateObject<IAccount>();
        dx.Name = "DevExpress";
    
        ICase case1 = ObjectSpace.CreateObject<ICase>();
        case1.Subject = "1";
        case1.Contact = roman;
        Assert.IsNull(case1.Account);
    
        ICase case2 = ObjectSpace.CreateObject<ICase>();
        case2.Subject = "2";
        case2.Account = dx;
        Assert.IsNull(case2.Contact);
    
        roman.Account = dx;
        ICase case3 = ObjectSpace.CreateObject<ICase>();
        case3.Subject = "3";
        case3.Contact = roman;
        Assert.AreEqual(dx, case3.Account);
    }

    [DomainComponent]
    public interface ICase {
        string Subject { get; set; }
        IAccount Account { get; set; }
        IContact Contact { get; set; }
    }

    To make this test pass we need an extra code, in addition to the interface. We need domain logic for the ICase to initialize the ICase.Account property by the ICase.Contact's Account:

    [DomainLogic(typeof(ICase))]
    public class CaseLogic {
        public static void AfterChange_Contact(ICase self) {
            if (self.Contact != null && self.Account == null) {
                self.Account = self.Contact.Account;
            }
        }
    }

    Here is how we are planning to add domain logic to the classes that are generated by the XpoBuilder. We are going to use naming conventions. If the XpoBuilder finds a method named “AfterChange_XXX”, it tries to call this method in the generated setter of a XXX property. We'll probably add the attribute-based binding in the future.

    A Case should have a human-readable ID that can be used in communications to quickly reference the Case. This ID should be generated only once, when a new object is beeing created. In addtion, the ID property should be read-only, but persistent.

    Here is one more DC design challenge: how domain logic can specify the initial value of a read-only property? Should I add an AfterConstruction method to the business logic to set a value? But in this instance, I'll have to provide some system interfaces to call the ID’s setter. So, I think it is better to implement the Init_XXX domain logic methods that will allow you to set a property's initial value for a newly created object.

    Since the ID property doesn't have a setter, it won't be persisted by default. To make this property persistent, I've decided to decorate it with the PersistentDc attribute. It could make sence to call this attribute Persistent, but in XAF this name conflicts with XPO’s Persistent attribute. So, it will be PersistentDc for now, and we will see what to do with it later.

    [DomainComponent]
    public interface ICase {
        [PersistentDc]
        string ID { get; }
        
    }
    [DomainLogic(typeof(ICase))]
    public class CaseLogic {
        public static string Init_ID(ICase self) {
            //ToDo: Change to "select count(*)+1 from case" analog
            return Guid.NewGuid().ToString();
        }
        
    }

    This is a very simplistic implementation. I will return with a more realistic code, as soon we have a proper infrastructure.

  • SDN Conference next week

    Just a quick note: I'll be speaking at the SDN Conference in Noordwijkerhout in the Netherlands next week. We're not exhibiting there, but of course I'll be looking out for DevExpress customers nevertheless - so if you're there, make sure to say hi! Of course I'll also appreciate you coming to my sessions on F# :-)

  • XCRM: Leads and opportunities

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    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.

    A Lead is not directly linked to a Contact or Account. It can be converted into an Account, Contact or Opportunity later. I’ll just capture the fact that the Lead class exists and may have some basic info:

    [Test]
    public void LeadsRelationships() {
        RegisterDC<ILead>();
        Generate();
        ILead romanFromDx = ObjectSpace.CreateObject<ILead>();
        romanFromDx.FirstName = "Roman";
        romanFromDx.LastName = "Eremin";
        romanFromDx.CompanyName = "DevExpress";
    }
    [DomainComponent]
    public interface ILead {
        string FirstName { get; set; }
        string LastName { get; set; }
        string CompanyName { get; set; }
    }

    At this point, I’m not sure that the ILead should be inherited from the IPerson. So, I've just added the FirstName and LastName properties, and made a note to resolve this problem later.

    An Opportunity is a more probable possibility for business. It is related to an Account (who you want to sell to) and Product (what you want to sell). Simple CRM systems do not track products and prices, so I will leave them out in our CRM app. Here is the test for Opportunities:

    [Test]
    public void OpportunityAccountRelationships() {
        RegisterDC<IContact>();
        RegisterDC<IAccount>();
        RegisterDC<IOpportunity>();
        Generate();
        IAccount dx = ObjectSpace.CreateObject<IAccount>();
        dx.Name = "DevExpress";
        IOpportunity sellComponents = ObjectSpace.CreateObject<IOpportunity>();
        sellComponents.Name = "Sell some third-party components to DX";
        sellComponents.Account = dx;
        Assert.IsTrue(Enumerator.Exists<IOpportunity>(dx.Opportunities, sellComponents));
    }

    To make this test pass I need the following:

    [DomainComponent]
    public interface IOpportunity {
        string Name { get; set; }
        IAccount Account { get; set; }
    }
    [DomainComponent]
    public interface IAccount {
        
        IList<IOpportunity> Opportunities { get; }
    }

  • XCRM: Writing real-world CRM application

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    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.

    I'm starting to write a generic CRM application, to try the new domain component (DC) technology and decide what classes should be included in our Domain Component technology. This application should be suitable for small business and extendable to support specific markets. While writing, I'm sure to find bottlenecks in XAF that we will have to fix.

    In general, a CRM application should be able to manage the following things:

    • Contacts
    • Accounts
    • Leads
    • Opportunities
    • Products
    • Appointments/Calendar/Tasks
    • Service Requests
    • Marketing Campaigns

    In addition, even a basic CRM system should have a security system that is aware of organization structure.

    To emphasize the most interesting design parts, I won't detail every domain class. I will create most classes in brief form, and then extend them as required. In the first iteration, I will focus on class relationships.

    Contact. Can belong to an Account. So, it has the Account property.

    Account. Can belong to another Account. So, it has the ParentAccount and Subaccounts properties. In addition, it should have a Contacts list, and a PrimaryContact, which may not belong to the Contacts list.

    I’m using TDD, so I will capture this knowledge in unit tests.

    Unit tests for Domain Components

    To write tests for domain components, I think I will need a DC infrastructure to be able to generate components from interfaces and business logic. I won't use the entire XAF – I will need only small parts of its services. Here is an example of a test:

    [DomainComponent]
    public interface IBasicTestThing {
        string Name { get; set; }
    }
    [TestFixture]
    public class BasicTests : BaseTest {
        [Test]
        public void TestBasicThing() {
            RegisterDC<IBasicTestThing>();
            Generate();
            IBasicTestThing thing1 = ObjectSpace.CreateObject<IBasicTestThing>();
            thing1.Name = "abc";
            ObjectSpace.CommitChanges();
            IBasicTestThing thing2 = ObjectSpace.FindObject<IBasicTestThing>(null);
            Assert.AreEqual("abc", thing2.Name);
        }
    }
    

    In all my tests, I'll register and generate domain components, and use an ObjectSpace object. So, all my tests will be inherited from the BaseTest class that will provide the required services. Look how it's implemented now:

    public class BaseTest {
        private IObjectSpace objectSpace;
        private IObjectSpaceProvider osProvider;
    
        [SetUp]
        public virtual void SetUp() {
            XafTypesInfo.Reset();
            osProvider = new ObjectSpaceProvider(new MemoryDataStoreProvider());
            objectSpace = osProvider.CreateObjectSpace();
        }
        public void RegisterDC<T>() {
            if (typeof(T).IsInterface) {
                XafTypesInfo.Instance.AddEntityToGenerate(typeof(T).Name, typeof(T));
            } else {
                XafTypesInfo.Instance.RegisterEntity(typeof(T));
            }
        }
        public IObjectSpace ObjectSpace {
            get { return objectSpace; }
        }
        public void Generate() {
            XafTypesInfo.Instance.GenerateEntities();
        }
    }
    

    The SetUp method cleans up the current type information, and creates an Object Space using a newly created Object Space Provider (using in-memory data store). The RegisterDC and Generate methods work with the XafTypesInfo system, like it's performed in XAF.

    Capturing domain knowledge in unit tests

    I’m ready to write a test for the Contact - Account relationship:

    [Test]
    public void ContactAccountRelationships() {
        RegisterDC<IContact>();
        RegisterDC<IAccount>();
        Generate();
        IContact roman = ObjectSpace.CreateObject<IContact>();
        roman.FirstName = "Roman";
        roman.LastName = "Eremin";
        IAccount dx = ObjectSpace.CreateObject<IAccount>();
        dx.Name = "DevExpress";
    
        dx.PrimaryContact = roman;
        Assert.IsNull(roman.Account);
    
        dx.Contacts.Add(roman);
        Assert.AreEqual(dx, roman.Account);
    
        IAccount xafTeam = ObjectSpace.CreateObject<IAccount>();
        xafTeam.ParentAccount = dx;
        Assert.IsTrue(Enumerator.Exists<IAccount>(dx.Subaccounts, xafTeam));
    }
    

    Enumerator is a helper class from the DevExpress.ExpressApp.Utils.dll assembly. It provides utility methods for the IEnumerable interface (similar extension methods already exist in .net 3.0).

    Here is the code that makes this test pass (remember – I’m focused on relationships, so don’t tell me that the IContact should contain the IPerson interface) :

    
    
    [DomainComponent]
    public interface IAccount {
        string Name { get; set; }
    
        IAccount ParentAccount { get; set; }
        IList<IAccount> Subaccounts { get; }
    
        IContact PrimaryContact { get; set; }
        IList<IContact> Contacts { get; }
    }
    
    [DomainComponent]
    public interface IContact {
        IAccount Account { get; set; }
    }

    No logic is required, because the XpoBuilder automatically generates the correct associations and XPO manages them. So, when I add roman to the dx.Contacts list, the roman.Account property is initialized automatically.

  • DC: Implemented interfaces in BOModel

    This post may be outdated. For the latest Domain Components concepts and examples refer to the current online documentation.

    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.

    While playing with CRM domain models, I've got a component like this:

    	[DomainComponent]
    	public interface IContact : IPersonalContactInfo, IPerson {
    		IAccount Account { get; set; } 
    	}

    This led me to one more issue that prevents XAF from working with the new models. I found out that the BOModel node doesn’t provide information about implemented interfaces. I would like XAF to treat implemented interfaces as it currently treats base classes – include base members into generated Views and reuse View Info provided for base classes.

    So, to start, I need information about implemented interfaces in the BOModel. To make a long story short – here it is:

    DC4-01

    The next step is to make members of the implemented interfaces visible in Views.

LIVE CHAT

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, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners