Blogs

Gary's Blog

August 2009 - Posts

  • Has Google Been out Googled by Realtime Web Search?

         

    In this edition of Outside the DXperience we’re going to take a look at the Realtime web; find out what it is and how search differs between the Traditional Web and the Realtime Web; we’ll look at who the players are in realtime search and who amongst them, if any, has the upper hand; before looking at the question on everyone’s lips… has Google been out Googled?

    What is the Realtime Web?
    Well put simply, the Realtime Web is the here and now. Where the traditional web consists of pages from commercial sites, blog posts – both amateur and professional – news sites, weather sites and pages of information of every conceivable topic; they all have one thing in common: they are in the past tense. Someone has conceived them, written them, polished them and finally published them. By the time you read it the information contained in the pages of the Traditional Web is at least hours old.

    The Realtime Web, on the other hand, is the web of the here and now. Fuelled by social media tools like Facebook and Twitter that allow users to post their thoughts at any given time, on any given topic, the Realtime Web is a window on the life of the global citizenry. Posts on the Realtime Web are never really conceived but are more off the cuff remarks; the thinking out loud of any given, ordinary, person. These posts are certainly never polished but, instead, are a measure of the writer’s emotion at that time. Posts on the Realtime Web are usually what we call “soft” posts, containing opinion rather than fact; emotion rather than measured response. They are useful because they are what people think without the “white lie” that most of us use to survive in our day to day social interactions.

    Of course, saying posts on the Realtime web are “soft” is a generalisation, one that holds true in most circumstances certainly, but a generalisation nevertheless. One area where this is not true of course is in the area of citizen journalism. When some “Johnny on the spot” can post words and sometimes pictures too of events as they happen, in a way that no news corporation could do, unless they were very, very fortunate. One example of this has been the coverage of the Iranian election, where most, if not all, of the most interesting coverage has come from people inside the country getting the word out via Realtime Web tools like Twitter and YouTube. Another place where you see the Realtime Web excel compared to the Traditional Web is during a natural disaster, where news of earthquakes are commonly first heard on Twitter or FriendFeed before you hear about them on the mainstream news channels.

    What is the difference Between Traditional Web Search and Realtime Web Search?
    Traditional Web search works by having a “crawler” (a software agent) examine as many web pages as it can and index those pages in large server farms. Then, when someone makes a query, the index is examined and matching results are returned, based on an algorithm that each search engine company hopes will out perform the other. At the moment, the undisputed king of Traditional Web search is Google.

    Searching the Realtime web works differently. You don’t have to be a genius to realise that it would not be possible to store and index every piece of Realtime Web information as it is generated. Instead Realtime Web search relies on making API calls to social media service providers and have them provide a list of “what just happened now and in the recent past”. Twitter, for example, limit the results of any search to the previous seven days. 

    Who are the Players in RealTime Web Search?
    Until recently, the three major players in Realtime Web Search were Twitter, Facebook and Friendfeed. This, of course, has been whittled down to just two now that Friendfeed have been bought by Facebook. There are, of course, a number of smaller players, here are a few in their own words:

    Scoopler is a real-time search engine. We aggregate and organize content being shared on the internet as it happens, like eye-witness reports of breaking news, photos and videos from big events, and links to the hottest memes of the day. We do this by constantly indexing live updates from services including Twitter, Flickr, Digg, Delicious and more. When you search for a topic on Scoopler, we give you the most relevant results, updated in real-time.

    OneRiot crawls the links people share on Twitter, Digg and other social sharing services, then indexes the content on those pages in seconds. The end result is a search experience that allows users to find the freshest, most socially-relevant content from across the realtime web.

    TweetMeme is a service which aggregates all the popular links on twitter to determine which links are popular. TweetMeme is able to categorize these links into categories and subcategories, making it easy to filter out the noise to find what your interested in.

    Who has the Upper Hand?
    Of the two big players, Twitter and Facebook, it is hard to say who has the upper hand at the moment as both have their advantages, which I’ll summarise here for you now:

    Twitter

    It was Twitter who was first to market and that gives them the all important “first mover advantage”. There are millions of people who know Twitter search and reach for it as those in the realm of the Traditional Web reach for Google. Add to that the fact that Twitter has more experience, they’ve been in the game for a year now, learning what works and – more importantly perhaps – what doesn’t. This means that they have the knowledge to respond quickly to whatever Facebook can bring to the market place. But, to my mind, Twitter’s main advantage is that it is an open platform.

    Facebook

    Despite what you might have heard, size does matter and, in those terms, Facebook is way ahead. Facebook has over 250 million users. With those people to draw on, Facebook can provide a more accurate picture of what people are talking about at any given moment. You are also, statistically, more likely to have a friend on Facebook than on Twitter. Facebook’s search covers a wider range of media; as well as statuses Facebook search can deliver video and pictures as well which, by the way, can be filtered.

    Has Google Been Out Googled?
    Let’s be honest, Google was a fluke. Don’t look at me like that, it was! They started out with the idea of indexing the web and somewhere along the road they found that they could make money by placing adverts on the result pages of people’s searches. Fair play to the lads at Google, they exploited that discovery to the max and made a fortune doing it. What Google did was to shift the “value base” of content. Before Google came along (and to a certain extend before the web) the “value base” was with the content providers. “He who writes the words earns the money” as it were. However, Google shifted that “value base” to be “he who finds the content earns the money”.

    With the advent of the Realtime Web, that “value base” has shifted again to be “he who provides the tools earns the money”. We established above that no company – not even the mighty Google – can index all the Realtime Web content that is produced every second of every day, and we are going to become heavily dependant on the tool vendors to provide an API which we, or search engine companies, can access to gain results for a search query. Successful social media tool vendors can charge what they like to search engine companies like Google, Yahoo and Microsoft to have access to that API. This, I feel, is the most likely revenue stream for providers such as Twitter and if Google are not very careful here, they could very well find themselves out Googled.

  • XAF – Project Management Application #7

         

    This latest post in our series on building a project management application in XAF starts a sort of series within a series. Here we begin to look at adding the concept of statuses to our application. As this is a big topic, I don’t intend to bore you all to death by tackling it in one mega post. Instead, we’ll look at it over the next few posts. In this post however we’ll make a start. What we want to do here is to allow ProjectTasks to have a Status. Task Statuses should be configurable for different task types. Each status should also have an index, this will allow us to display them in a particular order. For example, an engineering task could have these statuses: Draft -> Planned -> Elaboration -> Development -> Documenting -> Completed. Also, some statuses should be considered “final”. This would mean that a whole task is assumed to be completed when such a status is specified. For a generic project task, the statuses could be: Draft -> Not Started -> In Progress -> Paused -> Completed.

    Before we get started on our statuses though, there’s a piece of work we have to do first and that is to implement the functionality that will allow us to categorize tasks into functional areas. Each functional area will have a name and will be owned by someone. So let’s implement that now by, firstly, creating the FunctionalArea entity:

    [DefaultClassOptions]
    [NavigationItem("Settings")]
    [DefaultProperty("Name")]
    public class FunctionalArea : BaseObject {
        public FunctionalArea(Session session) : base(session) { }
    
        public string Name {
            get { return GetPropertyValue<string>("Name"); }
            set { SetPropertyValue<string>("Name", value); }
        }
        public Employee Owner {
            get { return GetPropertyValue<Employee>("Owner"); }
            set { SetPropertyValue<Employee>("Owner", value); }
        }
    }

    Then we’ll add the FunctionalArea property to the ProjectTask:

    public FunctionalArea FunctionalArea {
        get { return GetPropertyValue<FunctionalArea>("FunctionalArea"); }
        set { SetPropertyValue<FunctionalArea>("FunctionalArea", value); }
    }

    This will make the following update to our winforms and webforms application GUIs:

    2

     1

    Now, with that done, let’s focus on our statuses. The easiest way to implement this would be as an enumeration:

    public enum TaskStatus
    {
        Draft,
        NotStarted,
        InProgress,
        Paused,
        Completed
    }

    Of course doing this has an number of advantages: it’s quick to implement; the status property on the task is automatically initialised to the initial enumeration value and the last value can easily be considered to be the “final” status, as we spoke about above. However, implementing it this way does also have a number of disadvantages: we can’t use different status sets for different types or categories of tasks and a user could easily move from one status to another status when that status change is invalid; for example, from “draft” to “complete”.

    3

    4

    Because of this, we will implement the status as a business class:

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

    We also spoke of the idea of a TaskStatus belonging to a TaskStatusSet, so let’s go ahead and implement that too:

    public class TaskStatusSet : BaseObject {
        
        public TaskStatusSet(Session session) : base(session) { }
        public TaskStatusSet(Session session, string name)
            : base(session) {
            Guard.ArgumentNotNullOrEmpty(name, "name");
            Name = name;
        }
    
        public string Name {
            get { return GetPropertyValue<string>("Name"); }
            set { SetPropertyValue<string>("Name", value); }
        }
        [Association, Aggregated]
        public XPCollection<TaskStatus> Items {
            get { return GetCollection<TaskStatus>("Items"); }
        }
    }

    Notice a one-to-many association between these two classes. The TaskStatusSet class has the aggregated Items collection holding the TaskStatus objects which form a particular TaskStatusSet. In the ProjectTask class we introduce the TaskStatusSet property. The Status property is marked with the DataSourceProperty attribute, so that only a status from the current status set can be selected:

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

    Now when we launch the app and create a new project task we’ll see that the Status and the StatusSet fields are empty and uninitialized since we haven’t created any objects of these types:

    6

    7

    Let’s fix this by extending the ProjectTask class so that it synchronizes its status list with the statuses from the database. In the future we’ll have different kinds of task, all inheriting from some form of task base class; when that time comes we’ll refactor this code into that base class, but for now, we’ll implement this code in the ProjectTask class:

    public static void UpdateDatabase(Session session) {
        TaskStatusSet statusSet = session.FindObject<TaskStatusSet>(new BinaryOperator("Name", StatusSetName));
    
        if(statusSet == null) {
            statusSet = new TaskStatusSet(session, StatusSetName);
            statusSet.Save();
        }
        List<string> currentStatuses = new List<string>(Enum.GetNames(typeof(ProjectTaskStatuses)));
    
        foreach(TaskStatus storedStatus in statusSet.Items) {
            if(currentStatuses.Contains(storedStatus.Name)){
                currentStatuses.Remove(storedStatus.Name);
            }
        }
        if(currentStatuses.Count != 0) {
            foreach(string statusName in currentStatuses) {
                session.Save(new TaskStatus(session, statusSet, statusName));
            }                
        }
    }
    public enum ProjectTaskStatuses {
        Draft,
        NotStarted,
        InProgress,
        Paused,
        Completed
    }

    Then we’ll add a call to this method in the Updater class:

    public class Updater : ModuleUpdater {
        public Updater(Session session, Version currentDBVersion) : base(session, currentDBVersion) { }
        public override void UpdateDatabaseAfterUpdateSchema() {
            base.UpdateDatabaseAfterUpdateSchema();
    
            //…
            ProjectTask.UpdateDatabase(Session);
        }
    }

    Which makes the following changes to the GUI:

    8

    9

    Well this works, sort of, but it’s not perfect is it? We’ll fix some of the issues now and leave others for future posts. When a ProjectTask is created the StatusSet field is initialized, let’s create a test for this:

    [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);
        }
    }

    To make this test pass, we have to make the following change to he ProjectTask class:

    public override void AfterConstruction() {
        base.AfterConstruction();
    
        this.StatusSet = FindTaskStatusSet(Session);
    }
    private static TaskStatusSet FindTaskStatusSet(Session session) {
        return session.FindObject<TaskStatusSet>(new BinaryOperator("Name", StatusSetName));
    }

    Also, it makes no sense that the ProjectTask.Status field is visible in UI. To hide it we’ll apply a couple of attributes to it (VisibleInDetailView and VisibleInListView):

    [VisibleInDetailView(false), VisibleInListView(false)]
    public TaskStatusSet StatusSet {
        get { return GetPropertyValue<TaskStatusSet>("StatusSet"); }
        set { SetPropertyValue<TaskStatusSet>("StatusSet", value); }
    }

    The StatusSet field should only be initialized via the constructor, so we’ll mark the setter as private:

    [VisibleInDetailView(false), VisibleInListView(false), Persistent]
    public TaskStatusSet StatusSet {
        get { return GetPropertyValue<TaskStatusSet>("StatusSet"); }
        private set { SetPropertyValue<TaskStatusSet>("StatusSet", value); }
    }

    And, to be on the safe side, we’ll accompany this latest change with a test:

    [Test]
    public void StatusSet_HasValueAfterReloadFromDatabase() {
        ProjectTask task;
        TaskStatusSet taskStatusSet;
    
        using (UnitOfWork unitOfWork = new UnitOfWork()) {
            task = new ProjectTask(unitOfWork);
            taskStatusSet = task.StatusSet;
            Assert.IsNotNull(taskStatusSet);
            unitOfWork.CommitChanges();
        }
        using (UnitOfWork unitOfWork = new UnitOfWork()) {
            ProjectTask newTask = unitOfWork.GetObjectByKey<ProjectTask>(task.Oid);
            TaskStatusSet newTaskStatusSet = unitOfWork.GetObjectByKey<TaskStatusSet>(taskStatusSet.Oid);
            Assert.IsNotNull(newTaskStatusSet);
            Assert.AreEqual(newTaskStatusSet, newTask.StatusSet);
        }
    }

    When we run this test we see that XPO (the underlying ORM which XAF uses to persist our entities) has stopped persisting the StatusSet property’s value? Why? What happened there? Well it’s because we marked the setter as private, d’oh! Not to worry, we can easily fix this by applying the Persistent attribute:

    [VisibleInDetailView(false), VisibleInListView(false), Persistent]
    public TaskStatusSet StatusSet {
        get { return GetPropertyValue<TaskStatusSet>("StatusSet"); }
        private set { SetPropertyValue<TaskStatusSet>("StatusSet", value); }
    }

    The last issue that we are going to fix in this post is that the Status field doesn’t have a value assigned by default. To correct this, we need to extend the StatusSet class, to define the initial and final state. So that we can use the initial state as the default:

    public TaskStatus FirstStatus {
        get { return GetPropertyValue<TaskStatus>("FirstStatus"); }
        set { SetPropertyValue<TaskStatus>("FirstStatus", value); }
    }
    public TaskStatus LastStatus {
        get { return GetPropertyValue<TaskStatus>("LastStatus"); }
        set { SetPropertyValue<TaskStatus>("LastStatus", value); }
    }

    And to initialize these properties, we’ll extend the ProjectTask class’ UpdateDatabase method:

    public static void UpdateDatabase(Session session) {
        TaskStatusSet statusSet = session.FindObject<TaskStatusSet>(new BinaryOperator("Name", StatusSetName));
    
        if(statusSet == null) {
            statusSet = new TaskStatusSet(session, StatusSetName);
            statusSet.FirstStatus = new TaskStatus(session, statusSet, ProjectTaskStatuses.Draft.ToString());
            statusSet.LastStatus = new TaskStatus(session, statusSet, ProjectTaskStatuses.Completed.ToString());
            statusSet.Save();
        } //…

    }

    Finally, to set the initial Status when a ProjectTask is created, we need to update the AfterConstruction method as follows:

    public override void AfterConstruction() {
        base.AfterConstruction();
    
        this.StatusSet = FindTaskStatusSet(Session);
        this.Status = this.StatusSet.FirstStatus;
    }
    Phew, well that was quite a lot to fit into one blog post so I think we’ll end it there. In the coming posts we’re going to be resolving the following issues with statuses: currently, statuses are sorted by their name when displayed, while they should be logically ordered; a user can change any status to whatever other status he likes (e.g. Completed -> NotStarted) and lastly, the “New” and “Clear” actions shouldn’t be available in the lookup when changing the status. As always, you can download the source code to this project. Until next time, happy XAFing!
  • XAF – Project Management Application #6

         

    In this, the sixth post, in our series on creating a project management application in XAF, we’ll be adding functionality that will allow us to categorize our employees by team and also build tree like task categories. But before we do that let’s go back and revisit some of the code we’ve already written.

    First, we'll add a public constructor to our custom progress bar control that’ll configure the bar to show a value as well as a visualization of the amount of progress made:

    public RepositoryItemTaskProgressBarControl() {
    Maximum = 100;
    Minimum = 0;
    ShowTitle = true;
    Appearance.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center;
    }

    Secondly, at the moment, we’ve implemented the Project.Progress property without any logic, so let’s go ahead and change that such that the progress is calculated from the ProjectTasks associated with the Project. For the time being we’ll use a “quick and dirty” implementation, just to get things working:

    public float Progress {
    get {
    float sum = 0;
    int count = 0;
    foreach (ProjectTask task in Tasks) {
    sum += task.Progress;
    count++;
    }
    return (count == 0) ? 0 : sum / count;
    }
    }

    Now that this property is calculated we’ll use our custom progress bar Property Editors (Windows Forms and ASP.NET Web ) to display it:

    1

      2

    Finally, let’s add a unit test to test this functionality:

    public void ProjectProgress() {
    Project proj = new Project(Session);
    proj.Name = "test";
    Assert.AreEqual(0, proj.Progress);

    ProjectTask task1 = new ProjectTask(Session);
    task1.Project = proj;
    task1.Name = "task1";
    task1.PlannedHours = 10;
    TaskWork work1 = new TaskWork(Session);
    work1.Task = task1;
    work1.HoursSpent = 2;
    Assert.AreEqual(2f / 10f, proj.Progress);

    ProjectTask task2 = new ProjectTask(Session);
    task2.Name = "task2";
    task2.Project = proj;
    task2.PlannedHours = 20;
    TaskWork work2 = new TaskWork(Session);
    work2.Task = task2;
    work2.HoursSpent = 7;
    Assert.AreEqual((2f / 10f + 7f / 20f) / 2f, proj.Progress);
    }

    Next we want to create a mechanism by which we can have a local copy of the data when we are working on the winfoms version of our application. To do this we’ll create a Windows Forms specific View Controller to read/write data to/from XML files. This Controller has two Actions defined – DumpToXML and LoadDumpFromXML. These Actions have their category set to “Tools” and appear in the Tools menu when it’s invoked from root List Views:

    3

    4

    public partial class DumpDataController : ViewController {
    public DumpDataController() {
    InitializeComponent();
    RegisterActions(components);
    }

    private void LoadDumpFromXML_Execute(object sender, SimpleActionExecuteEventArgs e) {
    OpenFileDialog openFile = new OpenFileDialog();
    openFile.Filter = "XML files (*.xml)|*.xml|All files (*.*)|*.*";
    if (openFile.ShowDialog() == DialogResult.OK) {
    LoadDumpFromFile(openFile.FileName);
    }
    }
    private void DumpToXML_Execute(object sender, SimpleActionExecuteEventArgs e) {

    OpenFileDialog openFile = new OpenFileDialog();
    openFile.CheckFileExists = false;
    openFile.FileName = View.ObjectTypeInfo.Name;
    openFile.Filter = "XML files (*.xml)|*.xml|All files (*.*)|*.*";
    if (openFile.ShowDialog() == DialogResult.OK) {
    DumpToFile(openFile.FileName);
    }
    }

    private void DumpToFile(string fileName) {
    Session sourceSession = View.ObjectSpace.Session;
    DataSet dataSet = new DataSet();
    InMemoryDataStore dataStore = new InMemoryDataStore(dataSet, AutoCreateOption.DatabaseAndSchema);
    IDataLayer dataLayer = new SimpleDataLayer(sourceSession.Dictionary, dataStore);
    UnitOfWork destinationSession = new UnitOfWork(dataLayer);
    ClonerHelper.Clone(sourceSession, destinationSession, View.SelectedObjects);
    destinationSession.CommitChanges();
    dataSet.WriteXml(fileName, XmlWriteMode.WriteSchema);
    }
    private void LoadDumpFromFile(string fileName) {
    UnitOfWork destinationSession = View.ObjectSpace.Session as UnitOfWork;
    if (destinationSession == null) return;

    DataSet dataSet = new DataSet();
    InMemoryDataStore dataStore = new InMemoryDataStore(dataSet, AutoCreateOption.DatabaseAndSchema);
    IDataLayer dataLayer = new SimpleDataLayer(destinationSession.Dictionary, dataStore);
    dataSet.ReadXml(fileName);
    UnitOfWork sourceSession = new UnitOfWork(dataLayer);
    ClonerHelper.Clone(sourceSession, destinationSession, View.ObjectTypeInfo.Type);
    destinationSession.CommitChanges();
    }
    }

     

    Having done that, let’s set about enabling us to categorize Employees by Team. Firstly, let’s create a Team entity:

    [DefaultClassOptions]
    public class Team : BaseObject {
    public Team(Session session) : base(session) { }

    public string Name {
    get { return GetPropertyValue<string>("Name"); }
    set { SetPropertyValue<string>("Name", value); }
    }
    [Association]
    public XPCollection<Employee> Members {
    get { return GetCollection<Employee>("Members"); }
    }
    }

    Then let’s create a property, typed as Team, in our Employee class:

    [Association]
    public Team Team {
    get { return GetPropertyValue<Team>("Team"); }
    set { SetPropertyValue<Team>("Team", value); }
    }

    This adds the following GUI enhancements to our winforms and webforms applications:

    5

    6

    The next piece of functionality that we want to add is to have a tree-like hierarchy for our task categories; each Task should be of a certain category and all the categories should form a nice hierarchy. To achieve this we’ll create an entity called TaskCategory and we’ll inherit that class from the built in XAF class HCategory, this will allow us to easily display the TaskCategory in our Tree List Editor. At the same time, we’ll add a Category property to the ProjectTask Class:

    [DefaultClassOptions]
    [NavigationItem("Settings")]
    public class TaskCategory : HCategory {
    public TaskCategory(Session session) : base(session) { }
    [Association]
    public XPCollection<ProjectTask> Tasks {
    get { return GetCollection<ProjectTask>("Tasks"); }
    }
    }
    [Association]
    public TaskCategory Category {
    get { return GetPropertyValue<TaskCategory>("Category"); }
    set { SetPropertyValue<TaskCategory>("Category", value); }
    }

    Now let’s add the TreeListEditor module to our application. To do this we need to add the Windows Forms specific TreeListEditor module (TreeListEditorsWindowsFormsModule) to the Windows Forms application project and the ASP.NET Web specific TreeListEditor module (TreeListEditorsAspNetModule) to the ASP.NET Web application project. Having done this, the TaskCategory List Views will be automatically represented by the Tree List Editors. This is because the class derives from the HCategory which implements the ITreeNode interface. When this is done, our applications will look like this:

    7

     

     

    8

    When ProjectTasks are grouped in the list view by category the UI doesn’t make it very informative. Let’s fix that now by adding the full path property to the TaskCategory and making it the default property:

    [DefaultProperty("FullPath")]
    public class TaskCategory : HCategory {
        //…
    public string FullPath {
    get {
    TaskCategory parentCategory = Parent as TaskCategory;
    string parentPath = parentCategory != null ? parentCategory.FullPath : "";
    return parentPath + "/" + Name;
    }
    }
    }

    9

    Right, I think we’ll finish there for this post. As always, you can download the full source code for this application from here. Don’t forget to join us next time as we continue this series; until then, happy XAFing!

  • XAF – Project Management #5.1

         

    5.1? 5.1?! I hear you ask, what happened to #6?! Don’t worry, it’ll be along tomorrow; but for the moment I thought it would be a good idea to further explain some of the things that happened in #5. By that I mean that you will have noticed that we decided to do particular things and I explained how to do those things, but I never really explained why we’d done them that way. I noticed that it took long enough to detail with what we were doing and I didn’t want to turn a long post into an epic post by going into the whys and wherefores. This post, of course, is a blank sheet so why don’t we have a look at some of the decisions that were made in post #5?

    The first decision we took was to use HTML Editors to allow our users to format the description of a Project and a ProjectTask. Why did we do that and not use a RichEdit control for example? Well the reason is simple, we wanted to have the same behaviour in both the Webform and Winform application and, currently, DevExpress doesn’t have a RichEdit control for ASP.Net. :-)

    The next thing you may have found a little puzzling was when we came to replace the default images with images from the XAF library. I told you to change the image view via the model editor but I never told you where exactly that was done, did I? Well it’s done here:

    Tutorial_UIC_Lesson4_1[1]

    Also I didn’t mention what images we shipped. Well if you want to browse them for yourself you can find them at: <Program Files>\DevExpress XXXX.X\eXpressApp Framework\Sources\DevExpress.ExpressApp\DevExpress.ExpressApp.Images\Images.

    Of course, I also didn’t mention how to add your own custom images. Luckily, I covered that in a previous post here.

    Another thing you might have found puzzling was the when we came to format the date field on TaskWork we used this format:

    <Member Name="Date" DisplayFormat="{0:g}" />

    But where’d we get that format from? Well the format uses the String.Format() rules which you can read more about on the MSDN site.

    We then went on to do some work on the notification, why did we do that? Well it was simply because without that the UI would not update, although the underlying property would change. Calling OnChange allows the binding to reload the property from the business object and update the UI. Strictly speaking of course this code should not reside in the business object, as it’s not the business object’s responsibility to be concerned about the UI, but this is the price we pay for having the business object bound directly to the UI, this will be examined in future releases of XAF.

    In the tests you will notice the abstract class BaseXpoTest, which does some useful set up work for us. This base class can now be used by any other application we are working on in the future and there is talk, in the framework team, about maybe even making this as part of a future XAF/XPO release.

    Following this we went on to create our own TaskProgressbarControl to visualize the progress of our project, but why did we do that, I mean DevExpress already ships with a ProgressBarControl right? Yeah it does, but if you look at it then you’ll find that this control expects an int for progress and we wanted to use a float (between 0 and 1), so we had to derive our own control. We also derived our own RepositoryItem – what’s that all about? Well, all in-place editors need a RepositoryItem and so we had to derive one of our own, you can read up on the RepositoryItem class for more information, if you so desire.

    Of course whenever you “eat your own dog food” you can be left with a nasty taste in your mouth. Building this application has shown us that all is not rosy in the XAF garden. Specifically, you may have noticed that the text in the progress bar is not centred, why not? Well it turns out that someone hard-coded the alignment code in XAF to override the control's defaults and now, for float, it is right alignment regardless of what the control claims. Oops, as we speak that programmer is being tarred and feathered and we’re looking at fixing this in the future.

    Also, you may have noticed these lines of code:

    protected override WebControl CreateEditModeControlCore() {
       TaskProgressBar result = new TaskProgressBar();
       result.Width = Unit.Percentage(100);
       return result;
    }

    In particular the line result.Width = Unit.Percentage(100). Now this is a hack to compensate for problems we have in some browsers, this will be fixed for the 9.3 release.

    The last thing we discovered, in building this application, is that there is no easy way to show the progress bar in the web list view; again we will fix this for the 9.3 release.

    Now you can see why I didn’t put all this information in the original post, as I thought it was quite long enough as it was. Okay, so I’ll be back tomorrow with post #6, until then keep on XAFing! :-)

  • 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!

  • XAF – Project Management Application #4

         

    Continuing the evolution of our Project Management Application it’s about time we added an Employee, after all someone has to do the work, right? Not only do we want our Employee class to model a worker on our team, but we also want it to be able to implement some security. As you’ll know, when you create an XAF application an Active Directory based security strategy is used by default. The user in this strategy is SimpleUser and so it makes sense for us to inherit our Employee from this class. As for the properties of our Employee, for the time being, we’ll just settle for DisplayName, a property we’ll use so that our class can display something meaningful when asked.

    Looking at the SimpleUser we find there are two properties that contain a name (UserName and FullName) neither of which are mandatory. It seems obvious that FullName will be the more readable of the two so we’ll make our DisplayName property return the FullName if it is used and UserName if not. We’ll also set the DisplayName property as the default property.

    [DefaultProperty("DisplayName")]

        public class Employee : SimpleUser {

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

            public override string ToString() {

                return DisplayName;

            }

     

            public string DisplayName {

                get { return String.IsNullOrEmpty(FullName) ? UserName : FullName; }

            }

        }

    Now that we’ve created our Employee, we’ll go ahead and configure the security system. Actually, most of the work is already done for us as we inherited from SimpleUser which implements all the necessary interfaces. The only thing we have to do is to change the user type in the Winform and Webform projects via the application designers:

    1

    We’ll also create a couple of test employees via a module updater. This isn’t strictly necessary of course as the Active Directory Authentication Component creates users at logon, by default, if they don’t already exist, but what the heck, let’s do it anyway. :-)

    public override void UpdateDatabaseAfterUpdateSchema() {

                base.UpdateDatabaseAfterUpdateSchema();

     

                Employee user1 = Session.FindObject<Employee>(CriteriaOperator.Parse("UserName = 'TEST\\cooper'"));

                if(user1 == null) {

                    user1 = new Employee(Session);

                    user1.UserName = "TEST\\cooper";

                    user1.FullName = "Charlotte Cooper";

                    user1.Save();

                }

                Employee user2 = Session.FindObject<Employee>(CriteriaOperator.Parse("UserName = 'TEST\\ohno'"));

                if(user2 == null) {

                    user2 = new Employee(Session);

                    user2.UserName = "TEST\\ohno";

                    user2.FullName = "Mayumi Ohno";

                    user2.Save();

                }

            }

    2  7

    So, now that we have employees and we’ve tied them into the security module it’s time to use them. Firstly, let’s tell the Project that it’s going to have a Project Manager:

    public class Project : BaseObject {

     

            //...       

     

            public Employee Manager {

                get { return GetPropertyValue<Employee>("Manager"); }

                set { SetPropertyValue<Employee>("Manager", value); }

            }

        }

    3  8

    And we’ll let the ProjectTask know that it’s going to be assigned to someone:

    public class ProjectTask : BaseObject {

     

            //...

     

            public Employee AssignedTo {

                get { return GetPropertyValue<Employee>("AssignedTo"); }

                set { SetPropertyValue<Employee>("AssignedTo", value); }

            }

        }

    4  9

    Now that we can assign project tasks to an Employee we’d better create a quick way for each Employee to see the tasks assigned to him:

    5 6

    We’ll do this by creating List View Filters. We’ll create two, the first one wont filter anything but will show all tasks by default, the second one will show only tasks assigned to the current user. We can do this in two ways, firstly by using the ListViewFilterAttribute or by using the Model Editor.

    10

    The resulting code generated by the model editor is shown below for information:

    <?xml version="1.0" ?>

    <Application Title="XProject" Logo="ExpressAppLogo" >

      <Views>

        <ListView ID="ProjectTask_ListView">

          <Filters CurrentFilterID="MyTasks" IsNewNode="True">

            <Filter ID="AllTasks" Caption="All Tasks" Description="Show all project tasks" Index="1" IsNewNode="True" />

            <Filter ID="MyTasks" Caption="My Tasks" Index="0" Description="Show project tasks assigned to you" Criteria="AssignedTo.Oid = '@CurrentUserID'" IsNewNode="True" />

          </Filters>

        </ListView>

      </Views>

    </Application>

    Well, all this talk of project tasks has tired me out, so I’m off for a lie down now. As always you can examine the code for this post in Code Central. Until next time, enjoy XAF!

  • XAF – Project Management Application Index

         

    This page will serve as an index for the series of posts on creating the Project Management Application in XAF

    Post #1 – Setting the Scene
    http://community.devexpress.com/blogs/garyshort/archive/2009/06/25/xaf-project-management-application.aspx

    Post #2 – Working with Estimates
    http://community.devexpress.com/blogs/garyshort/archive/2009/07/27/xaf-project-management-application-2.aspx

    Post #3 – Working with Project and ProjectTask
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/10/xaf-project-management-application-3.aspx

    Post #4 – Working with Employee and Security
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/12/xaf-project-management-application-4.aspx

    Post #5 – Working with time tracking
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/18/xaf-project-management-application-5.aspx

    Post #5.1 – Explaining some of the decisions in post #5
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/19/xaf-project-management-5-1.aspx

    Post #6 – Categorizing Employees and Tasks
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/24/xaf-project-management-application-6.aspx

    Post #7 – Adding Statuses to Tasks
    http://community.devexpress.com/blogs/garyshort/archive/2009/08/24/xaf-project-management-application-7.aspx

    Post #8 – Continuing Work on Statuses
    http://community.devexpress.com/blogs/garyshort/archive/2009/09/02/xaf-project-management-application-8.aspx

    Post #9 – More Work on Statuses
    http://community.devexpress.com/blogs/garyshort/archive/2009/09/15/xaf-project-management-application-9.aspx

    Post #10 – Refactoring Statuses
    http://community.devexpress.com/blogs/garyshort/archive/2009/09/30/xaf-project-management-application-10.aspx

  • Playing in the Google Wave Sandbox

         

    Back in June I wrote a blog post about Google Wave the new collaborative platform from Google. Well, on my return from holiday I found my sandbox account had arrived, so along with many other lucky developers, I have early access to the Wave platform. Over the weekend I took a few hours out to “play with my new toy” and I thought I’d let you know what I found in this episode of Outside the DXperience.

    First things first, I’m running Windows 7 and so have Internet Explorer 8 installed by default. It turns out that Wave doesn’t much like IE8:

    Warning

    “Fair enough” I thought, but decided to push on regardless – a true technology pioneer me! Well it transpires that Google wasn’t exaggerating and, indeed, Wave doesn’t behave itself too well under IE8 – I managed to elicit this great error message from the Wave World:

    MayExplode

    Not much wanting to “explode” I decided to install Chrome and log back into Wave, whereupon I found all was well with the world:

    WavePage

    In the image above, you can see an example of what most people are using Wave for at the moment and that is as a structured, inline, collaborative chat client. Structured because you can see all the conversations, in the Wave, laid out before you but you can elect to hide replies from certain conversations so that you can concentrate on others. Inline because you can make a reply at any stage of the conversation and your reply is injected right there, you don’t have to top post or bottom post and collaborative because anyone you choose can join in the conversation and the Wave is persisted on the Wave Server for all participants to refer to in the future. Beware though, at the moment there is no mechanism to remove people from a Wave once you have added them. :-)

    As you can see, one of the Waves that I have subscribed to is one created by a group of Dundee based developers. This is the sort of thing that Wave was built to do. Dundee is the computer games capital of the UK; it also has a strong bio-medical presence and as such there are a lot of developer groups. This Wave (along with a Google Group) helps each group to stay in touch with each other and collaborate on common projects like Software Freedom Day. Here you can see a conversation on the Wave about the Horn project:

    HornChat

    Of course Wave is not all about collaborative chat, there are also bots for example. So what is a bot in the Wave world? Well Google say:

    A robot is an automated participant on a wave. A robot can read the contents of a wave in which it participates, modify the wave's contents, add or remove participants, and create new blips and new waves. In short, a robot can perform many of the actions that any other participant can perform.

    Of course there is an API to allow developers to create these bots – some of which are extremely useful. Others… not so much: :-)

    SweedishChefBot

    There are also Gadgets, which are “the standard way to embed non-trusted code in Google web applications”.

    Gadgets

    but the most interesting thing (I think anyway) is the ability to embed Waves in your own applications. Google says:

    The Google Wave Embed API allows you to easily (and quickly) add communication and collaboration tools to your web applications. The Embed API allows you to embed waves on third party websites, anywhere in the web, for easy discussion and collaboration using wave's cutting-edge user interface. Any conversations on those waves also show up in your Wave client, making Wave an easy way to aggregate conversations you care about all over the web in one place.

    And they are right, it’s fun and easy to use. In fact I’ve created a web page myself with a little demo Wave on it. Of course to see it and to participate in it, you’ll have to have a Google Wave Sandbox account, but if you do then stop by and add a comment to the wave. For those of you without sandbox accounts, the page looks like this:

    image

    All this and it even works on my iPhone too:

    006

    Well that’s enough playing with toys. I dare say I’ll write more on this subject as the platform matures, but for now, I better get back to the day job! :-)

  • XAF – Project Management Application #3

         

    Welcome back to this series of posts on the creation of of our project management application. In this episode we are going to concentrate on building two classes, Project and ProjectTask. As the name suggests, Project describes our project and has a collection of ProjectTasks, which describe each of the tasks associated with a given Project. Both of these classes have the same properties: Name, StartDate, EndDate, Description and Progress.

    Okay, now that we know what our classes are going to look like, let’s go ahead and build them, first the Project class:

    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.BaseImpl;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.Validation;
    
    namespace XProject.Module.Entities {
        [DefaultClassOptions]
        public class Project : BaseObject {
            public Project(Session session)
                : base(session) {}
    
            public string Name {
                get { return GetPropertyValue<string>("Name"); }
                set { SetPropertyValue<string>("Name", value); }
            }
            public DateTime StartDate {
                get { return GetPropertyValue<DateTime>("StartDate"); }
                set { SetPropertyValue<DateTime>("StartDate", value); }
            }
            public DateTime EndDate {
                get { return GetPropertyValue<DateTime>("EndDate"); }
                set { SetPropertyValue<DateTime>("EndDate", value); }
            }
            public float Progress {
                get { return GetPropertyValue<float>("Progress"); }
                set { SetPropertyValue<float>("Progress", value); }
            }
            public string Description {
                get { return GetPropertyValue<string>("Description"); }
                set { SetPropertyValue<string>("Description", value); }
            }
            [Association]
            public XPCollection<ProjectTask> Tasks { get { return GetCollection<ProjectTask>("Tasks"); } }
    
        }
    }

    And then the ProjectTask class:

    Imports Microsoft.VisualBasic
    Imports System
    Imports System.Collections.Generic
    Imports System.Text
    Imports DevExpress.Xpo
    Imports DevExpress.Persistent.BaseImpl
    Imports DevExpress.Persistent.Base
    Imports DevExpress.Persistent.Validation
    
    Namespace XProject.Module.Entities
        <DefaultClassOptions> _
        Public Class ProjectTask
            Inherits BaseObject
            Public Sub New(ByVal session As Session)
                MyBase.New(session)
            End Sub
    
            Public Property Name() As String
                Get
                    Return GetPropertyValue(Of String)("Name")
                End Get
                Set(ByVal value As String)
                    SetPropertyValue(Of String)("Name", value)
                End Set
            End Property
            Public Property StartDate() As DateTime
                Get
                    Return GetPropertyValue(Of DateTime)("StartDate")
                End Get
                Set(ByVal value As DateTime)
                    SetPropertyValue(Of DateTime)("StartDate", value)
                End Set
            End Property
            Public Property EndDate() As DateTime
                Get
                    Return GetPropertyValue(Of DateTime)("EndDate")
                End Get
                Set(ByVal value As DateTime)
                    SetPropertyValue(Of DateTime)("EndDate", value)
                End Set
            End Property
            Public Property Progress() As Single
                Get
                    Return GetPropertyValue(Of Single)("Progress")
                End Get
                Set(ByVal value As Single)
                    SetPropertyValue(Of Single)("Progress", value)
                End Set
            End Property
            Public Property Description() As String
                Get
                    Return GetPropertyValue(Of String)("Description")
                End Get
                Set(ByVal value As String)
                    SetPropertyValue(Of String)("Description", value)
                End Set
            End Property
            <Association> _
            Public Property Project() As Project
                Get
                    Return GetPropertyValue(Of Project)("Project")
                End Get
                Set(ByVal value As Project)
                    SetPropertyValue(Of Project)("Project", value)
                End Set
            End Property
        End Class
    End Namespace

    Before we go on, you’ll notice some good news for the VB developers amongst us, I’ve decided to include some VB code samples too so that it will be easier for the VB’ers to follow along with us. It’s going to get a little boring if I include both C# and VB code samples each time, so what I’ll do is alternate them, if that’s okay with you guys? (if you’ve got a better idea on how to do it, let me know in the comments).

    Now, the main thing to notice about the code snippets above are the two attributes we’ve made use of. The first of these [DefaultClassOptions] ensures that our new entity will be added to the “Default” node in the Navigation bar and that a corresponding item is added to the “New” action’s items list. It will also ensure that the entity is added as a data type in the New Report Wizard’s data type drop down list. The second, [Association] allows us to declare a one to many relationship between Project and ProjectTask (more on that later).

    Building and running our application at this stage, gives us the following WinForm behaviour:

    Screen1 

    And WebForm behaviour:

    Screen2

    Looking at these screen shots shows us a problem we have with the default implementation of our properties. By default XAF limits the size of text fields to 100 characters (actually this is a limit imposed by XPO, the underlying ORM tool that XAF sits on top of). 100 characters isn’t going to be any use for our description field, we’re going to want to be much more descriptive than that. Luckily we can tell XAF that we wish to store certain text fields in a “memo” field in the underlying database, by use of the Size attribute like so:

    [Size(SizeAttribute.Unlimited)]
    public string Description {
        get { return GetPropertyValue<string>("Description"); }
        set { SetPropertyValue<string>("Description", value); }
    }

    Screen3

    As you can see, having declared this attribute, XAF now generates a UI with a far more realistic field for “Description”.

    Now, as mentioned previously, because we’ve used the [DefaultClassOptions] attribute, our classes appears in the “Default” node of the navigation bar in our application. That’s not really ideal as it is not very descriptive; it would be much better if our entities were to appear in a “Planning” node say. We can achieve this by use of the [NavigationAttribute], like so:

    <DefaultClassOptions, NavigationItem("Planning")> _

        Public Class Project  

        <DefaultClassOptions, NavigationItem("Planning")> _

        Public Class ProjectTask

    Which results in the following change to our WinForm and WebForm navigation bar:

    Screen4

    Screen5

    Let’s turn our attention to validation now. As you can see we have both a StartDate and an EndDate and we should ensure that the EndDate is not before the StartDate and that the StartDate is not before today. After all, you can’t start work on a project before it’s created. To achieve this we simply add the validation module to the MySolution.Module project and apply the following [RuleCriteria] to the class signatures:

    [RuleCriteria("Project-EndDateMustBeAfterStartDate", DefaultContexts.Save, "EndDate > StartDate")]

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

        public class Project : BaseObject {

     

        [RuleCriteria("ProjectTask-EndDateMustBeAfterStartDate", DefaultContexts.Save, "EndDate > StartDate")]

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

        public class ProjectTask : BaseObject {

    Which will cause the built in validation engine within XAF to display the following errors, in our WinForm and WebForm applications, if the rules are not met:

    Screen6

    Screen7

    The last thing we are going to do before finishing this post is to tell a Project that it has a collection of ProjectTasks and to inform the ProjectTask class that it belongs to a Project. We do this by means of the [Association] attribute:

    <Association> _

    Public ReadOnly Property Tasks() As XPCollection(Of ProjectTask)

        Get

            Return GetCollection(Of ProjectTask)("Tasks")

        End Get

    End Property

    <Association> _

    Public Property Project() As Project

        Get

            Return GetPropertyValue(Of Project)("Project")

        End Get

        Set(ByVal value As Project)

            SetPropertyValue(Of Project)("Project", value)

        End Set

    End Property

    Doing this causes XAF to build a UI for us that will enable users to add ProjectTasks to a Project in both the WinForms and WebForms applications:

    Screen8

    Screen9

    Well that about wraps our look at the Project Management Application for this post. New in this episode, you can examine the source code for the first time too, so that you can follow along at home if you so wish. Don’t forget to join us for the next post in the series where we will continue the evolution of our application!

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.