in
Forums
Blogs
DevExpress.com
Client Center
Support Center
DevExpress Channel

Gary's Blog

  • How to Drink at the Twitter Firehose and not Drown

    Recently Twitter made the firehose feed available to the public via their API. Well I say that, they’ve made a 5% sample available to everyone, if you want anything above that then you have to apply directly to Twitter. But hey, 1 tweet in 20 is enough to be going on with, it still works out at around 25 tweets per second. :-)

    So let’s say we want to consume this feed how are we going to do it? Well first thing we have to do is to access the feed URL and make the connection, and handle all the HTTP stuff and… Meh, yeah that sounds boring to me, how about you? Thankfully we don’t have to do all that as it’s already been done for us by the nice guys at the cURL project. Go to the site and download it and we’ll have a nice little command line tool that is going to do all that HTTP stuff for us.

    After installing the tool and adding it’s home directory to your path, open a command window and type:

    curl http://stream.twitter.com/1/statuses/sample.json –uYourTwitterUserName:YourTwitterPassword

    and you’ll see all the tweets passing by in JSON format, like so:

    image

    Why JSON format? Well we could have asked for the feed in XML but there are two reasons why we didn’t. One, JSON is much more compact and bandwidth costs money and two, Twitter are considering XML for deprication from the streaming API so it’s best that we get used to using JSON from the start.

    Well so far so good, we’ve hooked up to the stream and we can see the tweets flying past but that doesn’t really do us any good. What we need to do is to consume this feed and we need to do it in such a way as we can keep up with the rate at which Twitter sends the information down the line. We really need a two part solution to this problem, firstly we need to read the feed and secondly we need to process the tweets from the feed. These two parts need to be completely decoupled.

    Let’s deal with the reading part first. What we need to do is to wrap the cURL tool and then post the raw JSON onto a queue, what happens after that we don’t need to worry about at the moment. We just need to keep pace with the flow from Twitter or they will cut our connection. We’ll do that with the following code:

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Messaging;
    
    class Program
    {
        static void Main()
        {
            //GS - Create a curl process so we can piggy back on its capabilities
            ProcessStartInfo curl = new ProcessStartInfo();
            curl.FileName = @"c:\program files\curl\curl.exe";
            curl.Arguments = http://stream.twitter.com/1/statuses/sample.json –uYourTwitterUID:YourTwitterPassword;
            curl.UseShellExecute = false;
            curl.RedirectStandardOutput = true;
    
            //GS - Get the message queue, create it if required
            MessageQueue q = 
                (MessageQueue.Exists(@".\private$\TwitterQueue")) ?
                new MessageQueue(@".\private$\TwitterQueue") :
                MessageQueue.Create(@".\private$\TwitterQueue");
    
            //GS - Start curl process
            using (Process process = Process.Start(curl))
            {
                using (StreamReader reader = process.StandardOutput)
                {
                    //GS - Post the output from curl to the queue.
                    //One line = one tweet in json format.
                    while (!reader.EndOfStream)
                    {
                        string result = reader.ReadLine();
                        Message message = new Message(result);
                        q.Send(message);
                    }               
                }
            }
        }
    }

    That code is pretty simple and well commented, so we wont say anymore about it. As you can see, it simply takes the output from cURL and posts it onto a MSMQ queue running on my local machine. Let’s run that for 20 seconds and see what happens:

    image

    We can see that running this script for ~20 seconds get’s us around half a meg of data from Twitter and results in about 1,000 messages being posted to our queue:

    image

    The next thing we are going to do is to consume this queue. We’ll have two issues in doing that. Firstly, we will have to process quickly or the queue will grow out of hand over time, and secondly, the messages have the tweet information in JSON format, and there is little or no JSON support in the .Net framework at this time. We’ll solve the first problem by reading the messages in and then processing them on a separate thread for speed and we’ll solve the second problem by downloading and using JSON.Net a third party library that handles all that JSON gloop very well. The code to consume the queue and store the tweet information in a database, for later reporting, looks like this:

    using System;
    using System.Messaging;
    using Newtonsoft.Json.Linq;
    using Newtonsoft.Json;
    using System.Data.SqlClient;
    using System.Data;
    using System.Threading;
    
    namespace Reader
    {
        class Program
        {
            static void Main()
            {
                //GS - Get the queue
                MessageQueue q;
                if(MessageQueue.Exists(@".\private$\TwitterQueue")) 
                {
                    q = new MessageQueue(@".\private$\TwitterQueue");
                }
                else
                {
                    //GS - If there is no queue then we're done here
                    Console.WriteLine("Queue has not been created!");
                    return;
                }
    
                //GS - Consume the queue
                while (true)
                {
                    //GS - Try and pull back the next message
                    Message message;
                    try
                    {
                        message = q.Receive();
                        message.Formatter =
                            new XmlMessageFormatter(new String[] 
                                { "System.String" });
    
                        //GS - Multi-thread for speed. Process the message on the 
                        //next available thread
                        ThreadPool.QueueUserWorkItem(ProcessMessage, message);
                    }
                    //GS - Any errors, skip and go to the next message
                    catch { continue; }
                }
            }
            private static void ProcessMessage(object stateInfo)
            {
                //GS - Try and process the message...
                try
                {
                    //GS - Turn stateInfo back into a message
                    Message message = stateInfo as Message;
    
                    //GS - Then turn the message into a JSON object
                    JObject obj = JObject.Parse(message.Body.ToString());
    
                    //GS - We don't want all the user info just the id and name
                    string userId = obj["user"]["id"].ToString();
                    string userName = obj["user"]["screen_name"].ToString();
                    obj.Remove("user");
    
                    //GS - Turn it into a CLR object. This is not strictly required 
                    //but the serializer will handle all of that nasty string or 
                    //null stuff for us and I'm lazy :-)
                    Tweet tweet =
                        new JsonSerializer().Deserialize(
                        new JTokenReader(obj),
                        typeof(Tweet)) as Tweet;
    
                    //GS - Add back the user id and screen name
                    tweet.User = userId;
                    tweet.ScreenName = userName;
    
                    //GS - Persist the tweet
                    using (SqlCommand command = new SqlCommand())
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.Connection = 
                            new SqlConnection("Data Source=.;Initial Catalog=" +
                                "TwitterFirehose;Integrated Security=True");
    
                        command.CommandText = "InsertTweet";
                        command.Parameters.AddWithValue("Contributors", 
                            tweet.Contributors);
    
                        command.Parameters.AddWithValue("Coordinates", 
                            tweet.Coordinates);
    
                        command.Parameters.AddWithValue("CreatedAt", 
                            tweet.Created_at);
    
                        command.Parameters.AddWithValue("Favourited", 
                            tweet.Favorited);
    
                        command.Parameters.AddWithValue("Geo", tweet.Geo);
                        command.Parameters.AddWithValue("Id", tweet.Id);
                        command.Parameters.AddWithValue("InReplyToScreenName", 
                            tweet.In_reply_to_screen_name);
    
                        command.Parameters.AddWithValue("InReplyToStatus", 
                            tweet.In_reply_to_status_id);
    
                        command.Parameters.AddWithValue("InReplyToUserId", 
                            tweet.In_reply_to_user_id);
    
                        command.Parameters.AddWithValue("Place", tweet.Place);
                        command.Parameters.AddWithValue("Source", tweet.Source);
                        command.Parameters.AddWithValue("Text", tweet.Text);
                        command.Parameters.AddWithValue("Truncated", 
                            tweet.Truncated);
    
                        command.Parameters.AddWithValue("User", tweet.User);
                        command.Parameters.AddWithValue("ScreenName", 
                            tweet.ScreenName);
    
                        command.Connection.Open();
                        command.ExecuteNonQuery();
                        command.Connection.Close();
                    }
                }
                //GS - Twitter will throw "keep alive" chars and delete requests 
                //down the wire so we wont be able to process every message. If 
                //there is an error processing a message, then we're done here.
                catch { return; }
            }
        }
    }

    This seems to work very well, in fact in tests this script consumes the queue faster than the first script can fill it, so we can be confident that we can run these two scripts and not run out of resources. Running these scripts for a couple of minutes garners us around 10,000 tweets in the database. With that information we can start to look at things like the most popular client for Twitter:

    image

    (Sample size: 10,116)

    Now that you know how to do this with the sample feed, you can experiment on your own with the filter feed and start to chart things that are important for your organisation; things like who posts the most about your product, are the influencers in your market taking about you, if so are they talking about you more or less than last month etc. Also, you will will notice that we capture geo information too, so if you combine this post with my last post you can see that you can start to get a physical picture of where in the world your most vocal customers are. If you filter for reputation (whether posts are negative or positive) you might be able to see a problem with a particular office as your map may show a cluster of negative comments around a particular city. The possibilities are endless and I’ll leave you to experiment on your own, but rest assured I am archiving all Twitter mentions of DevExpress and our products, so remember to say nice things about us. :-)

    I’ll leave you with this pre launch checklist from Twitter that will help you consume the feed and not fall foul of their connection police:

    Pre-Launch Checklist

    1. Not purposefully attempting to circumvent access limits and levels?
    2. Creating the minimal number of connections?
    3. Avoiding duplicate logins?
    4. Backing off from failures: none for first disconnect, seconds for repeated network (TCP/IP) level issues, minutes for repeated HTTP (4XX codes)?
    5. Using long-lived connections?
    6. Tolerant of other objects and newlines in markup stream? (Non <status> objects...)
    7. Tolerant of duplicate messages?
    8. Using JSON if at all possible?

    Until next time, happy coding!

  • XPO, Bing, Twitter and the TARDIS

    As you may know, Twitter recently introduced geocoding into their tweets, so now when you tweet, if you do so from a device with GPS capabilities, your position is recorded along with your post. That got me thinking… since I travel around a lot, I wonder if I can map my position on Bing maps.

    First I thought I could just import my twitter stream as Bing maps says it supports the <geo:point> element, but this didn’t work. I think that may be down to some XSS protection built into Bing maps, though I’m not 100% certain as I didn’t bother to delve into it after I found that it didn’t work. Mainly because it’s not the best solution due to the fact that Twitter ages your posts and eventually they will “fall off the end”. So if you want to be able to record your locations (via your tweets) for a whole year say, then you are going to have to store that information yourself, and that’s what we are going to look at in this post.

    So first thing we need is a class to hold the information we want to store, which is Date (including the time) as well as latitude and longitude, kind of the Time And Relative Dimension In Space. Now, I wonder what we could call such a class… :-) Anyway, the class looks like this:

    using System;
    using DevExpress.Xpo;
    
    namespace sharedModel
    {
        public class Tardis : XPObject
        {
            public Tardis(Session session) : base(session) { }
    
            private DateTime date;
            public DateTime Date
            {
                get
                {
                    return date;
                }
                set
                {
                    SetPropertyValue("Date", ref date, value);
                }
            }
    
            private decimal lat;
            public decimal Lat
            {
                get
                {
                    return lat;
                }
                set
                {
                    SetPropertyValue("Lat", ref lat, value);
                }
            }
    
            private decimal @long;
            public decimal Long
            {
                get
                {
                    return @long;
                }
                set
                {
                    SetPropertyValue("Long", ref @long, value);
                }
            }
    
            private long status_Id;
            public long Status_Id
            {
                get
                {
                    return status_Id;
                }
                set
                {
                    SetPropertyValue("Status_Id", ref status_Id, value);
                }
            }
        }
    }

    There isn’t anything here that you’ve not seen before so we’ll say no more about it, other than we’ll have to put it in a DLL project as we want to share it between a console and a web project that we are going to go ahead and create now.

    Firstly the console project, which is going to fetch and store our most recent tweets:

    using System;
    using System.Linq;
    using System.Xml.Linq;
    using DevExpress.Xpo;
    using sharedModel;
    
    namespace LocationFetcher
    {
        class Program
        {
            static void Main(string[] args)
            {
                //GS - Specify the SQL Server DB we want to use
                XpoDefault.Session.ConnectionString =
                    @"data source=.;integrated security=true;" + 
                        "initial catalog=TweetLocations;";
    
                //GS - Use a linq query to get that last status_id recorded
                XPQuery<Tardis> tardisQuery =
                    new XPQuery<Tardis>(XpoDefault.Session);
    
                var lastStatusId =
                    (from t in tardisQuery select (t.Status_Id)).Max();
    
                //GS - If there are no persisted tardis values, 
                //set the lastSessionId = 1 as Twitter can't handle 0
                lastStatusId = (lastStatusId == 0) ? 1 : lastStatusId;
    
                //GS - Fetch Tweets newer than lastStatusId
                XDocument timeline =
                    XDocument.Load(String.Format(
                        @"http://api.twitter.com/1/statuses/user_timeline.xml?"
                            + "screen_name=garyshort&since_id={0}", lastStatusId));
    
                //GS - If there have been no updates since last id then 
                //we're done here
                if (timeline.Descendants("status").Count<XElement>() == 0)
                {
                    Console.WriteLine("No new Tweets to process!");
                    return;
                }
    
                //GS - Add the georss namespaces
                XNamespace nsGeoRSS = "http://www.georss.org/georss";
    
                //GS - Persist the status information
                using (UnitOfWork uow = new UnitOfWork())
                {
                    //GS - Specify the DB we're using
                    uow.ConnectionString =
                        @"data source=.;integrated security=true;" +
                            @"initial catalog=TweetLocations;";
    
                    foreach (var status in timeline.Root.Descendants("status"))
                    {
                        //GS - If there is no geo tag then skip this status
                        if (status.Element("geo").IsEmpty)
                            continue;
    
                        new Tardis(uow)
                        {
                            Date = DateTime.ParseExact(status.Element("created_at")
                                .Value, "ddd MMM dd HH:mm:ss zz00 yyyy", null),
    
                            Status_Id = long.Parse(status.Element("id").Value),
    
                            Lat = Decimal.Parse(status.Element("geo").Element(
                                    nsGeoRSS + "point").Value.Split(" "
                                        .ToCharArray())[0]),
    
                            Long = Decimal.Parse(status.Element("geo").Element(
                                    nsGeoRSS + "point").Value.Split(" "
                                        .ToCharArray())[1]),
                        };
                    }
                    uow.CommitChanges();
                }            
            }
        }
    }

    Taking a look at this script can see that it’s pretty standard XPO stuff. Up front we tell XPO that we are going to be using a SQL Server database and where to find it. Then we use Linq to XPO to retrieve the latest status id and we only ask Twitter for tweets that were posted after this id, this should stop us from having duplicates in the database. After that, we simply iterate across the tweets returned by Twitter and store them in the database.

    The next thing we need to do is to create a web script that will return our stored TARDIS entries as a GeoRSS feed. The script to do that looks like this:

    using System;
    using System.Linq;
    using System.Xml;
    using System.Text;
    using DevExpress.Xpo;
    using sharedModel;
    
    public partial class GetTwitterLocations : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            //GS - Connect to the db and return all the locations ordered 
            //by status id - that's newest tweets last
            XPQuery<Tardis> tardisQuery =
                new XPQuery<Tardis>(XpoDefault.Session);
    
            var tweetLocations = from t in tardisQuery
                                 orderby t.Status_Id ascending
                                 select t;
            
            //GS - Clear any previous response and state we're returning XML
            Response.Clear();
            Response.ContentType = "text/xml";
    
            //GS - Instantiate an XML writer to use
            using (XmlTextWriter writer = new XmlTextWriter(Response.OutputStream, 
                Encoding.UTF8))
            {
                //GS - Set indentation level and start writing the document
                writer.Formatting = Formatting.Indented;
                writer.Indentation = 3;
                writer.Namespaces = true;
                writer.WriteStartDocument();
    
                //GS - Identify this as an RSS document
                writer.WriteStartElement("rss");
                writer.WriteAttributeString("version", "2.0");
    
                //GS - Add the GeoRSS namespace
                writer.WriteAttributeString("xmlns:geo", 
                    "http://www.w3.org/2003/01/geo/wgs84_pos#");
    
                //GS - Add in each of the locations
                foreach (var t in tweetLocations)
                {
                    writer.WriteStartElement("item");
                    writer.WriteElementString("title", t.Date.ToLongDateString());
                    writer.WriteElementString("geo:lat", t.Lat.ToString());
                    writer.WriteElementString("geo:long", t.Long.ToString());
                    writer.WriteEndElement();
                }
    
                //GS - Close tags, flush and send
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }
            Response.End();
        }
    }

    The first thing to note here is that we use the default session, but how does the default session get set to the SQL Server database that we are using? I mean you can’t set it in this method as the session is already established and XPO will throw and exception if you try to change the database after you have established the session. To avoid this you must set the database connection in the Global.asax file, like so:

    void Application_Start(object sender, EventArgs e) 
    {
        DevExpress.Xpo.XpoDefault.Session.ConnectionString =
                @"data source=.;integrated security=true;" +
                    @"initial catalog=TweetLocations;";
    
    }

    Next, you’ll note, we ask XPO for the TARDIS entries in ascending order. This is because I make numerous posts form the same location. In this example I don’t want to filter that list and only map one tweet per location, instead I want to ensure that the most recent one is “on top”. The rest of the script is fairly well commented and I don’t think it needs further explanation. Drop me a note in the comments if you have any questions.

    When you navigate to this page the feed is generated, like so:

    image

    Which means the only thing left to do is to write a web page which will show where I’ve been tweeting from recently. And that script looks like this:

    <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" 
        Inherits="_Default" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
       <head>
          <title>Where's Gary?</title>
          <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
          <script 
            src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2">
          </script>
    
          <script type="text/javascript">
              
              var map = null;
              
              //GS - Initialise the page
              function InitPage() {
                  GetMap();
                  AddMyLayer();
              }
                      
             //GS - Load the map
             function GetMap() {
                map = new VEMap('myMap');
                map.LoadMap();          
             }
    
             //GS - Add the layer and load in our tweet location data
             function AddMyLayer() {
                var l = new VEShapeLayer();
                var veLayerSpec = new VEShapeSourceSpecification(VEDataType.GeoRSS, 
                    "http://localhost:8341/twitterMap/GetTwitterLocations.aspx", l);
                map.ImportShapeLayerData(veLayerSpec);
             }
          </script>
       </head>
       <body onload="InitPage();">
          <div id="myMap" style="position:relative;"></div>
       </body>
    </html>

    Again this script is well commented and so you should be able to see exactly what is going on. Pointing your browser at this page shows the following map:

    image

    Here you can see that I’ve recently been to Germany and if you hover over the pins you can see the latest date that I was there:

    image

    Well that’s a bit of fun really but you can see how you can use this to instrument your social network if you so desired. Here I’m just showing the location of my own tweets, but you could just as easily map your followers with with greatest amount of retweets of your post, for example. The possibilities are endless and I’ll leave you to ponder those in your own time, but if you build something cool along these lines, with XPO, then leave a comment or drop me an email and I’ll highlight it on this blog.

    Although we are mapping Time And Relative Dimension In Space and we can vary our location (relative dimension) sadly this application will not yet let us vary time, so I guess we can’t compete with the Doctor just yet. Maybe that’ll be an 11.1 feature for XPO – until that does happen, happy XPOing! :-)

  • Keeping Fit With XAF #2

    In the first post in this series we looked at creating a WeightRecord. Today, we are going to look at adding TrainingProgrammes and TrainingRecords. First, however, we are going to tidy up the WeightRecord from last time and implement a suggestion from Chris Royle, to add BMI to the class.

    So once we’ve added BMI and take input of height in metres and weight in kilos, our class now looks like this:

    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    
    namespace Solution12.Module
    {
        [DefaultClassOptions]
        public class WeightRecord : BaseObject
        {
            public WeightRecord(Session session) : base(session) { }
    
            private DateTime _Date;
            public DateTime Date
            {
                get
                {
                    return _Date;
                }
                set
                {
                    SetPropertyValue("Date", ref _Date, value);
                }
            }
    
            private double _HeightInMetres;
            public double HeightInMetres
            {
                get
                {
                    return _HeightInMetres;
                }
                set
                {
                    SetPropertyValue("HeightInMetres", ref _HeightInMetres, value);
                }
            }
    
            private int _WeightInKilos;
            public int WeightInKilos
            {
                get
                {
                    return _WeightInKilos;
                }
                set
                {
                    SetPropertyValue("WeightInKilos", ref _WeightInKilos, value);
                }
            }
            
            [Persistent]
            public double BMI
            {
                get
                {
                    return (WeightInKilos == 0 || HeightInMetres == 0) ? 0 :
                        WeightInKilos / (HeightInMetres * HeightInMetres);
                }
            }
        }
    }

    Now we want to add two more classes. TrainingProgramme which will record activity on a particular exercise machine and TrainingRecord, which will record the date on which a particular TrainingProgramme was executed. These two classes look like this:

    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    
    namespace Solution12.Module
    {
        [DefaultClassOptions]
        public class TrainingProgramme : BaseObject
        {
            public TrainingProgramme(Session session) : base(session) { }
    
            private string _Machine;
            public string Machine
            {
                get
                {
                    return _Machine;
                }
                set
                {
                    SetPropertyValue("Machine", ref _Machine, value);
                }
            }
    
            private int _Level;
            public int Level
            {
                get
                {
                    return _Level;
                }
                set
                {
                    SetPropertyValue("Level", ref _Level, value);
                }
            }
    
            private int _DurationInMinutes;
            public int DurationInMinutes
            {
                get
                {
                    return _DurationInMinutes;
                }
                set
                {
                    SetPropertyValue("DurationInMinutes", ref _DurationInMinutes, value);
                }
            }
    
            private TrainingRecord _TrainingRecord;
            [Association("TrainingRecord-TrainingProgrammes")]
            public TrainingRecord TrainingRecord
            {
                get
                {
                    return _TrainingRecord;
                }
                set
                {
                    SetPropertyValue("TrainingRecord", ref _TrainingRecord, value);
                }
            }
        }
    }
    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    
    namespace Solution12.Module
    {
        [DefaultClassOptions]
        public class TrainingRecord : BaseObject
        {
            public TrainingRecord(Session session) : base(session) { }
    
            private DateTime _Date;
            public DateTime Date
            {
                get
                {
                    return _Date;
                }
                set
                {
                    SetPropertyValue("Date", ref _Date, value);
                }
            }
    
            [Association("TrainingRecord-TrainingProgrammes")]
            public XPCollection<TrainingProgramme> TrainingProgrammes
            {
                get
                {
                    return GetCollection<TrainingProgramme>("TrainingProgrammes");
                }
            }
        }
    
    }

    Giving us the following UI:

    image

    image

    So, that about wraps it up for today. Next time we’ll tidy up the UI and add reporting to our fitness application. Until then happy XAFing! :-)

  • Keeping Fit With XAF #1

    Hello XAFers! So January’s passed and all those great New Year’s resolutions for getting fit have fallen by the wayside. They have haven’t they, come on you can tell me? Yeah I thought so. Well don’t worry ‘cos I’m here to help. By, here to help, what I mean is that I too have a New Year’s resolution for losing weight and getting fit. Of course, when I start to lose motivation I turn to XAF to help me out, I mean what could be better than having a hot piece of software to help you? Okay, I admit, a hot personal trainer would be better, but it is what it is right?

    Anyway, today we’ll begin a short series of posts on an application that I threw together (yes I do mean threw together and not developed) to help me measure my weight lose and fitness program. So, the first order of business is going to be to record my weight as it heads down towards my target. Let’s add a domain object to our application and add properties for the date the record was made and my weight that day, in pounds:

    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    
    namespace Fitness.Module
    {
        [DefaultClassOptions]
        public class WeightRecord : BaseObject
        {
            public WeightRecord(Session session) : base(session) { }
    
            private DateTime date;
            public DateTime Date
            {
                get
                {
                    return date;
                }
                set
                {
                    SetPropertyValue("Date", ref date, value);
                }
            }
    
            private int lBS;
            public int LBS
            {
                get
                {
                    return lBS;
                }
                set
                {
                    SetPropertyValue("LBS", ref lBS, value);
                }
            }
       }
    }

    Okay, well working in pounds is fine for my American cousins, but here in the UK we work in stones and in Europe they work in Kilos, so we should really show those values too. We don’t want to input that information three times though and we don’t have to as we can calculate both of these fields from the lBS field. To do that, modify the class definition like so:

    using System;
    using DevExpress.Xpo;
    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    
    namespace Fitness.Module
    {
        [DefaultClassOptions]
        public class WeightRecord : BaseObject
        {
            public WeightRecord(Session session) : base(session) { }
    
            private DateTime date;
            public DateTime Date
            {
                get
                {
                    return date;
                }
                set
                {
                    SetPropertyValue("Date", ref date, value);
                }
            }
    
            private int lBS;
            public int LBS
            {
                get
                {
                    return lBS;
                }
                set
                {
                    SetPropertyValue("LBS", ref lBS, value);
                }
            }
    
            [Persistent]
            public double Stones
            {
                get
                {
                    const double STONES_PER_POUND = 0.0714285714;
                    return Math.Round((lBS == 0) ? 0 : lBS * STONES_PER_POUND,2);
                }
            }
    
            [Persistent]
            public double Kilos
            {
                get 
                {
                    const double KILOS_PER_POUND = 0.45359237;
                    return Math.Round((lBS == 0) ? 0 : lBS * KILOS_PER_POUND,2);
                }
            }
        }
    }

    We mark the property with the persistent attribute as we want the value to be saved in the database. Notice there is no setter though, we just do the calculation in the getter. There is one more thing we need to do for this to work and that is to set the ImmediatePostData attribute to true on those calculated fields in the WeightRecord node on the Model Editor. First find these fields in the Model Editor:

    image

    Then set the ImmediatePostDataAttribute to true:

    image

    This attribute specifies whether the property value is updated immediately when changes occur in the current Property Editor's bound control.

    Now, let’s fire up the application and we can see that when we enter an amount for the pounds, we get a value calculated for stones and kilos too:

    image

    Looking at the list view, we can see that these values are persisted to the database:

    image

    Well that wraps it up for this post, next time we’ll add some training programs to the application – until then, happy XAFing! :-)

  • XAF 10.1 Sneak Peek

    One of the major features that you will see as of 10.1 is the Typed Application Model.

    As you will know XAF allows developers to define business class and then builds the Application Model from these declarations and generates a UI based on them. Thereafter if you want to change the UI you simply customise the Application Model using the Model Editor. The Application Model is therefore a cornerstone of XAF and we have re-designed it from the bottom up in 10.1.

    Firstly, let’s take a look at what the Application Model is like now, prior to 10.1. It is currently defined by it’s schema and looks something like this:

    <Element Name="Application">
    <
    Element Name="Views">
    <
    Element Name="ListView">
    <
    Element Name="Variants" >
    <
    Attribute Name="Current" />
    <
    Element Name="Variant" >
    <
    Attribute Name="ID" />
    <
    Attribute Name="Caption" />
    <
    Attribute Name="ViewID" />
    </
    Element>
    </
    Element>
    </
    Element>
    </
    Element>
    </
    Element>

    This snipped declares the Variants node, as a child node of the ListView node. As you can see, in this simplified example, the only thing that attributes haveare names. There are no types declared. So, even if the ID attribute is intended to hold an integer and Caption to hold a string, there is really no difference. The untyped Application Model stores its data as a set of string. For a caption this is fine, for an integer though, it’ll first need to be converted to a string representation. When retrieving its value, it’ll need to be converted again from a string to an integer. What’s more, you need to remember that an integer is stored there, because all the Application Model stores are plain strings. What a PITA!

    Now let’s take a look at how the Application Model worked under the hood. This’ll be a pretty simplified look but it’ll do for our purposes. When an application is started, the types info subsystem (see HowTo: Access Business Class Metadata) collects metadata on all the business classes declared in the application. After this, the Application Model generation is started. Firstly, the BOModel node’s child nodes (see BOModel Node) are generated and filled with values from the types info subsystem. Then all the other Application Model’s nodes and their attributes are filled with data.

    After this initial generation is complete, the Application Model is filled with custom data from all the Modules used in the application.

    At this point we have the initial Application Model, that part which can’t be changed by the user. Then the Application Model is filled with each user’s customizations . Now, the Application Model generation is complete, and we can use our application.

    Now that we know how our untyped Application Model works, we can see the disadvantages:

    1. We COMPLETELY generate ALL THE Application Model at application startup. This is slow.
    2. Memory consumption is far from being optimal. Each attribute contains a string value, not a reference. Usually, there are a lot of duplicate info. For example, a string holding the name of a business class can be found in: types info subsystem, a BOModel node’s attribute, an attribute of each of the Views declared for the class (usually, there are several Views for each business type, at least two or three) and so on.
    3. The Application Model is untyped, and working with it isn’t straightforward. Though we can use node wrappers, the process is generally bulky.

    So, in general, the current Application Model, is suboptimal. :-)

    But it is improved in 10.1 and here’s how. What we’ve done is to do away with the schema and we now use interfaces instead. So the example above would now be declared like so:

    public interface IModelViewVariants : IModelNode
    {
    IModelVariants Variants { get; set; }
    }

    public interface IModelVariants : IModelNode,
    IModelList<IModelVariant>
    {
    IModelVariant Current { get; set; }
    }
    public interface IModelVariant : IModelNode
    {
    int Id { get; set; }
    string ViewID { get; set; }
    string Caption { get; set; }
    }

    Now as you see, attributes not only have names, but they also have types. Also, there’s no need to use node wrappers or convert attribute values to and from string representations. That is much better, don’t you think?

    The basic Application Model structure is described by the base IModelApplication interface. This interface defines the root Application node (its attributes and child nodes). Each Module can also extend the Application Model. This is done by specifying additional interfaces derived from the IModelNode interface.

    So, firstly, when an application is started, we collect all the interfaces that define the Application Model. Then we compile an object implementing all these interfaces. This object represents the Application Model. At this point the Application Model doesn’t hold any actual data.

    Secondly, we fill the Application Model with layers. Our new typed Application Model consists of unmerged layers. Each layer represents a separate data set. Each Module is represented by its own data layer. So, for each Module we create a separate layer and fill it with data supplied by the Module. We also create the user customizations layer and load the user customizations into it. Note that the base Application Model layer defined by the IModelApplication interface doesn’t contain any data at this point.

    At this point, the Application Model is ready to be used. When, for example, the Logon Detail View needs to be invoked, the Application Model is accessed, to retrieve the Detail View’s layout. The following code snippet illustrates this:

    IModelView modelView = 
    Application.ModelApplication.Views[
    “Logon_DetailView”];

    Since the Views node hasn’t been generated yet, a Views generator is invoked which in turn invokes the BOModel generator. As a result, the Views and BOModel nodes’ child nodes are created. Note however, that the created child nodes are NOT filled with data. Then, since we asked for the “Logon_DetailView” data, this node gets populated with data on the base layer. After this, XAF displays the specified View using the data.

    As you can see, the base layer of the Application Model is populated with data on-demand. In other words, if you launch an XAF application, and use only a Contact List View then only the Contact_ListView node will be populated with data. All the other View nodes of the Application Model will remain uninitialized.

    A note on ASP.NET Web XAF applications. Now, if users haven’t customized the Application Model, a single Application Model instance is shared between all the users. Moreover, even if users have customized the Application Model, the common part of it is still represented by a single instance which is also shared between the users.

    Now that we know how our typed Application Model works, we can outline its advantages:

    1. We never completely generate all the Application Model. It’s created on demand (kind of a lazy initialization). This is fast (or at least we hope it will be).
    2. Memory consumption is much more optimal. Each attribute holds a reference, not another value copy. So, for example, ideally, a string holding the name of a business class could only be found in the types info subsystem. In ASP.NET Web XAF applications, most of the Application Model (and sometimes ALL the Application Model) is shared between users.
    3. The Application Model is typed, and working with it becomes more straightforward. No more need for string conversions and use of additional artificial wrappers.

    Okay, so let’s finish up with some before and after examples:

    Defining a key:

    Before:

    <Element Name="Variant" KeyAttribute="ID" />
    After:
    [KeyProperty("Id")]
    public interface IModelEditorStateRule : IEditorStateRule {
    string Id { get; set; }
    }

    Declaring multiple child nodes:

    Before:

    <Element Name="ListView">
    <
    Element Name="Variants">
    <
    Element Name="Variant" Multiple="True" />

    After:

    public interface IModelViewVariants : IModelNode {
    IModelVariants Variants { get; set; }
    }
    public interface IModelVariants :
    IModelNode, IModelList<IModelVariant> {
    }
    public interface IModelVariant : IModelNode { .. }

    Declaring a localisable attribute:

    Before:

    <Element Name="Variant">
    <
    Attribute Name="Caption" IsLocalized="True" />

    After:

    public interface IModelVariant : IModelNode {
    [Localizable(true)]
    string Caption { get; set; }
    }

    Specifying an image for an node:

    Before:

    <Element Name="Variant" ImageName="ModelEditor_ListView">

    After:

    [ImageName("ModelEditor_ListView")] 
    public interface IModelVariant : IModelNode {...

    Specifying the display property:

    Before:

    <Element Name="Variant" DisplayAttribute="Caption" />

    After:

    [DisplayProperty("Caption")]
    public interface IModelVariant : IModelNode {...

    Specifying a required attribute:

    Before:

    <Element Name="Variant" >
    <
    Attribute Name="ViewID" Required="True" />

    After:

    public interface IModelVariant : IModelNode {
    [Required()]
    string ViewID { get; set; }

    Specifying a child node’s Index attribute used to order child nodes:

    Before:

    <Element Name="Variant" >
    <
    Attribute Name="Index" IsNewNode="True"/>

    After:

    Nothing to do here as the base IModelNode interface already declares such an attribute.

    Using an enumeration to specify the possible values for an attribute displayed in a dropdown:

    Before:

    <Element Name="NavigationItems" >
    <
    Attribute Name=""
    DefaultChildItemsDisplayStyle=""""
    Choice=""List,LargeIcons=""""/>

    After:

    public interface IModelNavigationItems : IModelNode {
    ItemsDisplayStyle DefaultChildItemsDisplayStyle { get; set; }

    Getting data from the Application Model. Retrieving a node:

    Before:

    DictionaryNode viewsNode = 
    Application.Model.RootNode.GetChildNode("Views");
    DictionaryNode viewNode =
    viewsNode.FindChildNode(
    BaseViewInfoNodeWrapper.IdAttribute, viewId);

    After:

    IModelView modelView =
        Application.ModelApplication.Views[viewId];

    Getting data from the Application Model. Retrieving an attribute value:

    Before:

    DictionaryNode viewNode = 
    viewsNode.FindChildNode(
    BaseViewInfoNodeWrapper.IdAttribute, viewId);

    string myCustomFilter =
    viewNode.GetAttributeValue("MyCustomFilter");

    After:

    IModelViewMyExtention myModelView = 
    (IModelViewVariants)
    Application.ModelApplication.Views[viewId];

    string myCustomAttrValue = myModelView.MyCustomFilter;

     

    Well that’s all for this post, until next time – happy XAF-ing! :-)

  • Santa Gets it Done With XPO

    After my last post I said we’d take a look at one to many associations next time, and that’s what we are doing to do now. With it coming up to Christmas I thought we would write a little application to give Santa a hand with his deliveries – I mean the poor old guy’s got a lot of work to do so I figure he could use all the help he can get, right?

    So we are going to write an application that will provide him with delivery instructions, detailing which presents to deliver to which house. To do that, the first thing we need is a class to define a house:

    using System;
    using DevExpress.Xpo;
    
    namespace XPOChristmas
    {
        public class House : XPObject
        {
            public House(Session session)
                : base(session)
            { }
    
            private string address;
            public string Address
            {
                get
                {
                    return address;
                }
                set
                {
                    SetPropertyValue("Address", ref address, value);
                }
            }
    
            private Country country;
            public Country Country
            {
                get
                {
                    return country;
                }
                set
                {
                    SetPropertyValue("Country", ref country, value);
                }
            }        
    
            [Association("House-Presents")]
            public XPCollection<Present> Presents
            {
                get
                {
                    return GetCollection<Present>("Presents");
                }
            }
    
            public void ShowDeliveryInstructions()
            {
                Console.WriteLine(String.Format("Santa, deliver to {0} the following: ", Address));
                foreach (Present p in Presents)
                {
                    Console.WriteLine(String.Format("   {0}", p.Description));
                }
                Console.WriteLine();
            }        
        }
    }

    As you can see, this class extends XPObject just like you’ve seen in all the XPO example so far. It also has a property detailing the address of the house. This is a simple string property that we’ve seen many times before, so we need not say any more about that.

    We need not say too much about the country property either, it simply holds an enum describing the country in which the house is located, the definition of the enum is very simple:

    using System;
    
    namespace XPOChristmas
    {
        public enum Country
        {
            Scotland,
            England,
            Ireland,
            Wales
        }
    }

    The next property however, is a little special. This property holds the child relationship with all the presents that are due to be delivered to this house. The first thing to notice about the property is that it is decorated with the Association attribute. This attribute must be unique in the database and the string name must match on both the parent and child ends of the association. Note the use of the helper function GetCollection<T> which returns all the children in the association.

    The last thing to notice about this class is the ShowDeliveryInstructions() method that displays instructions for the house and then walks the graph of children and displays instructions for them.

    The next thing we need then is a class to describe the presents:

    using System;
    using DevExpress.Xpo;
    
    namespace XPOChristmas
    {
        public class Present : XPObject
        {
            public Present(Session session)
                : base(session)
            { }
    
            private string description;
            public string Description
            {
                get
                {
                    return description;
                }
                set
                {
                    SetPropertyValue("Description", ref description, value);
                }
            }
    
            private House house;
            [Association("House-Presents")]
            public House House
            {
                get
                {
                    return house;
                }
                set
                {
                    SetPropertyValue("House", ref house, value);
                }
            }
    
        }
    }

    This class has a Description property which we need not say too much about and also a property which points to it’s parent house. Note the association attribute decorating the property and pay particular attention to the fact that the name is the same name that is on the parent end of the association.

    Once we have those two classes we just need code to drive it all:

    using System;
    using System.Linq;
    using DevExpress.Xpo;
    
    namespace XPOChristmas
    {
        class Program
        {
            static void Main(string[] args)
            {
    
                //GS - Persist some houses and presents for Santa
                using (UnitOfWork uow = new UnitOfWork())
                {
                    House house = new House(uow)
                    {
                        Address = "27 Cromarty Road",
                        Country = Country.Scotland
                    };
    
                    new Present(uow)
                    {
                        Description = "Little Blue Car",
                        House = house
                    };
    
                    new Present(uow)
                    {
                        Description = "Big Book For Boys",
                        House = house
                    };
    
                    House nextHouse = new House(uow)
                    {
                        Address = "27 Chelsea Lane",
                        Country = Country.England
                    };
    
                    new Present(uow)
                    {
                        Description = "Little Pink Car",
                        House = nextHouse
                    };
    
                    new Present(uow)
                    {
                        Description = "Big Book For Girls",
                        House = nextHouse
                    };
    
                    uow.CommitChanges();
                }
    
                //GS - Get delivery instructions for all houses
                Console.WriteLine("Displaying all houses...");
                Console.WriteLine();
                using (UnitOfWork uow = new UnitOfWork())
                {
                    XPQuery<House> houseQuery = new XPQuery<House>(uow);
    
                    foreach (var house in houseQuery)
                    {
                        house.ShowDeliveryInstructions();
                    }
                }
    
                //GS - Get delivery instructions for the house in Scotland
                Console.WriteLine("Displaying Scottish houses...");
                Console.WriteLine();
                using (UnitOfWork uow = new UnitOfWork())
                {
                    XPQuery<House> houseQuery = new XPQuery<House>(uow);
    
                    var houses = from h in houseQuery 
                                where h.Country == Country.Scotland 
                                select h;
    
                    foreach(var house in houses )
                    {
                        house.ShowDeliveryInstructions();
                    }
                }
    
            }
        }
    }

    In this code we persist some house and present objects and then retrieve them from the database before asking them to display their delivery instructions. There is nothing really special to say about this code, we have seen many such examples. The only thing to note is that the one to many associations can be specified from either end. In this case I specify it from the child side, because I found that most convenient, but either will do.

    Now that I have shown you the code, all that remains is to run it:

    image

    And that is it really, with such good help how could Santa fail to get all his presents delivered on time? That brings this post to a close, so until next time, happy XPOing! :-)

  • XPO Rocks Your World With Earthquakes

    In the last post I made, looking at analysing weather information using XPO, one of the commenters asked if we could see more examples using different types of associations between objects, so I thought I’d start off by posting something on one to one associations. To do this we are going to look at earthquakes (well why not?).

    Now the USGS are good enough to provide us with a feed of earthquakes in, pretty much, real time. Since the bigger the better, we’ll look at quakes magnitude 5 and above happening in the previous 7 days. The USGS publishes an atom feed of these at: http://earthquake.usgs.gov/earthquakes/catalogs/7day-M5.xml.

    To use this feed, the first thing we are going to do is to define an Earthquake entity and a GeoPoint entity, these entities will have a one to one relationship:

    using System;
    using DevExpress.Xpo;
    
    namespace EarthquakeFetcher.Model
    {
        public class EarthQuake: XPObject
        {
            public EarthQuake(Session session)
                : base(session)
            { }
    
            private string title;
            public string Title
            {
                get
                {
                    return title;
                }
                set
                {
                    SetPropertyValue("Title", ref title, value);
                }
            }
    
            private DateTime timeStamp;
            public DateTime TimeStamp
            {
                get
                {
                    return timeStamp;
                }
                set
                {
                    SetPropertyValue("TimeStamp", ref timeStamp, value);
                }
            }
    
            private GeoPoint geoPoint;
            public GeoPoint GeoPoint
            {
                get
                {
                    return geoPoint;
                }
                set
                {
                    if (geoPoint == value)
                        return;
    
                    //GS - Store a reference to the former geoPoint.
                    GeoPoint previousGP = geoPoint;
                    geoPoint = value;
    
                    if (IsLoading) return;
    
                    //GS - Remove the previous reference if there is one.
                    if (previousGP != null && previousGP.EarthQuake == this)
                        previousGP.EarthQuake = null;
    
                    //GS - Attach this reference
                    if (geoPoint != null)
                        geoPoint.EarthQuake = this;
    
                    //GS - Signal the change
                    OnChanged("GeoPoint");
                }
            }
    
            //GS - Override ToString() to give us a pretty label
            public override string ToString()
            {
                return Title;
            }
    
            
        }
    }
    using System;
    using DevExpress.Xpo;
    
    namespace EarthquakeFetcher.Model
    {
        public class GeoPoint : XPObject
        {
            public GeoPoint(Session session)
                : base(session)
            { }
    
            private string point;
            public string Point
            {
                get
                {
                    return point;
                }
                set
                {
                    SetPropertyValue("Point", ref point, value);
                }
            }
    
            private int elevation;
            public int Elevation
            {
                get
                {
                    return elevation;
                }
                set
                {
                    SetPropertyValue("Elevation", ref elevation, value);
                }
            }
    
            private EarthQuake earthQuake;
            public EarthQuake EarthQuake
            {
                get
                {
                    return earthQuake;
                }
                set
                {
                    if (earthQuake == value)
                        return;
    
                    //GS - Store a reference to the former EarthQuake.
                    EarthQuake previousQuake = earthQuake;
                    earthQuake = value;
    
                    if (IsLoading) return;
    
                    //GS - Remove the previous reference if there is one.
                    if (previousQuake != null && previousQuake.GeoPoint == this)
                        previousQuake.GeoPoint = null;
    
                    //GS - Attach this reference
                    if (earthQuake != null)
                        earthQuake.GeoPoint = this;
    
                    //GS - Signal the change
                    OnChanged("EarthQuake");
                }
            }
        }
    }

    Note the syntax on the one to one associations. Now then, the next thing we have to do is to read the feed, create objects from it and persist them:

    using System;
    using System.Xml.Linq;
    using DevExpress.Xpo;
    using EarthquakeFetcher.Model;
    
    namespace EarthquakeFetcher
    {
        class Program
        {
            static void Main()
            {                      
                //GS - Fetch the feed
                XDocument quakeFeed = 
                    XDocument.Load(@"http://earthquake.usgs.gov/earthquakes/catalogs/7day-M5.xml");
    
                //GS - Add the required namespaces
                XNamespace nsAtom = "http://www.w3.org/2005/Atom";
                XNamespace nsGeoRSS = "http://www.georss.org/georss";
    
                //GS - Persist the quake information
                using (UnitOfWork uow = new UnitOfWork())
                {
                    foreach (var quake in quakeFeed.Descendants(nsAtom + "entry"))
                    {
                        new EarthQuake(uow)
                         {
                             Title = quake.Element(nsAtom + "title").Value,
                             TimeStamp = DateTime.Parse(quake.Element(nsAtom + "updated").Value),
                             GeoPoint = new GeoPoint(uow)
                             {
                                 Point = quake.Element(nsGeoRSS + "point").Value,
                                 Elevation = Convert.ToInt32(quake.Element(nsGeoRSS + "elev").Value)                             
                             }
                         };
                    }
                    uow.CommitChanges();                
                }
            }
        }
    }

    There isn’t anything in the above code worth commenting on I think, just note the addition of the namespaces for atom and GeoRSS. Running this code will cause the present list of quakes to be persisted. Note, this code is for demonstration and instructional purposes only and has had “guard” code removed for clarity. If you were to put this code into production you would have to handle the case of duplicate quake entries being persisted if you were to run this code more than once in any 7 day period.

    Now that we have our persisted entities, it’s time to use them. First, we’ll create a view to display them:

    image

    The code behind this form looks like this:

    using System;
    using System.Windows.Forms;
    using EarthquakeFetcher.Model;
    using System.Diagnostics;
    
    namespace EarthquakeViewer
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void Form1_Load(object sender, EventArgs e)
            {
                FillListBox();
                SelectFirstItemInListBox();
            }
    
            private void FillListBox()
            {
                QuakeListBox.Items.AddRange(EarthquakeHelper.GetAllEarthquakes.ToArray());            
            }
    
            private void SelectFirstItemInListBox()
            {
                if(QuakeListBox.Items.Count > 0)
                    QuakeListBox.SelectedIndex = 0;
            }
    
            private void ShowOnMapButton_Click(object sender, EventArgs e)
            {
                EarthQuake quake = QuakeListBox.SelectedItem as EarthQuake;
                ShowQuakeOnMap(quake);
            }
    
            private static void ShowQuakeOnMap(EarthQuake quake)
            {
                if (!String.IsNullOrEmpty(quake.GeoPoint.Point))
                    Process.Start(String.Format(@"http://maps.google.com/?q={0}", quake.GeoPoint.Point));
            }
    
            private void MapMaxElevationButton_Click(object sender, EventArgs e)
            {
                ShowQuakeOnMap(EarthquakeHelper.GetHighestElevatedQuake);
            }
    
            private void MapMinElevationButton_Click(object sender, EventArgs e)
            {
                ShowQuakeOnMap(EarthquakeHelper.GetLowestElevatedQuake);
            }
        }
    }

    So, from the code we can see that we have a helper class, EarthquakeHelper, that gives us a few helper methods, let’s take a look at those now:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using DevExpress.Xpo;
    using EarthquakeFetcher.Model;
    
    namespace EarthquakeViewer
    {
        public class EarthquakeHelper
        {
            //GS - Return all quakes. This is safe as there will not be too many in a 7 day period.
            public static List<EarthQuake> GetAllEarthquakes
            {
                get
                {
                    XPQuery<EarthQuake> quakeQuery = new XPQuery<EarthQuake>(XpoDefault.Session);
                    return (from q in quakeQuery select q).ToList<EarthQuake>();
                }
            }
    
            public static EarthQuake GetHighestElevatedQuake
            {
                get
                {
                    XPQuery<EarthQuake> quakeQuery = new XPQuery<EarthQuake>(XpoDefault.Session);
                    return (from q in quakeQuery
                        orderby q.GeoPoint.Elevation descending
                        select q).First<EarthQuake>();
                }
            }
    
            public static EarthQuake GetLowestElevatedQuake
            {
                get
                {
                    XPQuery<EarthQuake> quakeQuery = new XPQuery<EarthQuake>(XpoDefault.Session);
                    return (from q in quakeQuery
                            orderby q.GeoPoint.Elevation ascending
                            select q).First<EarthQuake>();
                }
            }
        }
    }

    You can see that the GetAllEarthquakes property does what is says on the tin, it returns all of the quakes in the database. Again all “guard” has been removed for clarity and we are assuming that the database only contains the last 7 days worth of data. The other two properties use linq to XPO to return the highest and lowest earthquakes by elevation. These three properties return quakes that are then used by the three buttons on the form to show the location, via Google maps, of the required earthquakes:

    image

    This post is not only interesting from an XPO point of view, but also from an earthquake point of view. I mean, who of you knew there were so many earthquakes happening right now? Anyway, that’s it for this post, in the next one we’ll take a closer look at at one to many and many to many associations. Until then, happy XPOing!

  • That Was TechEd that Was!

    And what a busy week it was! with 7,300 attendees at the Messe in Berlin the exhibition hall was busy from the word go. Of course, it may have helped that Microsoft deliberately planned the conference to coincide with the 20th anniversary of the fall of the Berlin wall, but whatever it was TechEd 09 was a sell out event and that is a rare occurrence at the moment.

    The partner event on the Monday evening was busy, but then we expected it to be and we gave away most of our T shirts, in fact Rachel had to order reinforcements for the rest of the week! What we didn’t expect of course was that the rest of the week would be just as busy. There were literally hordes (that is the collective noun for developers, right?) of developers around the booth, eager to hear the latest news about their favourite product. In the past the main topic of interest as been WPF and Silverlight, those two platforms vying for supremacy amongst developer’s interest. However, the last two conferences I’ve been too, and again here, developers have been showing more and more interest in XAF. Of course, as the evangelist for XAF that is music to my ears. In fact at this conference the very first person I spoke to wanted a demonstration of XAF! So, go us!

    On Thursday I delivered a session to the architecture track on Technical Debt. It fits in quite well with the development issues that RefactorPro! can help you with and also, it deals with the interest on Technical Debt – something CodeRush can help you reduce. So, whilst the talk doesn’t directly promote our tools, my presentation speaks to the philosophy behind them. I’d never spoken to a TechEd audience before and I must admit I thoroughly enjoyed the experience. The atmosphere is quite different from other conference and user groups that I’ve spoken at. Very intense, and let’s just say, you’d better be on top of your game or the audience will call you out. :-)

    One of the highlights of any large conference is meeting old friends, both customers and other “techies”, that I only get to meet two or three times a year at the big conferences. One of them (thanks Betsy) took, what Rachel reliably informs me is, the only half decent photograph of me at TechED. Though I think she only said that cos she’s in it too LOL. You can always rely on your friends to bring you down to earth eh?

    And before we knew it, it was Friday and it was all over. Time to pack up the booth and say goodbye to old and new friends alike, buoyed by the knowledge that we’ll all be back to do it again next year, where I’m already looking forward to demonstrating the latest and greatest product line up from DevExpress. See you there!

  • XPO – Charting Local Weather Conditions #2

    In the last post on this topic I showed you how to pull down weather information from your local METAR station and persist it using XPO. In this post we’ll go ahead and use that persisted information to graph some interesting weather data.

    First thing we’re going to do is to add a new win forms project to our solution to provide a front end for our graphing application. We’ll add folders for Helpers, Model and Views too:

    image

    After that we’ll start the coding by pointing XPO at the Access database we created in the last post. We’ll do this in the form load event:

    private void MainView_Load(object sender, EventArgs e)
    {
        //GS - Connect to the access database created earlier
        XpoDefault.ConnectionString =
            AccessConnectionProvider.GetConnectionString("pathToYourAccessDatabaseFile");
    }

    Next we’ll create a UI that will allows us to graph three interesting pieces of weather data; wind speed by direction, temperature fluctuation throughout a day and the prevailing wind direction for this area:

    image

    Starting with wind speed by direction, what we’ll do is handle the button click event:

    private void btnNorth_Click(object sender, EventArgs e)
    {
        //GS - Display northerly wind speeds
        DisplayWindSpeedByDirectionWithTitle("Northerly Wind Speeds", "N");
    }

    by calling a method to display the data:

    private void DisplayWindSpeedByDirectionWithTitle(string title, string direction)
    {
        //GS - Get all the Northerly wind direction reports
        XPQuery<Report> reportQuery = new XPQuery<Report>(XpoDefault.Session);
    
        var reports = from r in reportQuery
                      where r.WindDirection.StartsWith(direction)
                      select r;
    
        //GS - Ask a helper method to create a chart from this info
        ChartControl chart = ChartHelper.CreateBarChartFromLinqResult("WindSpeed", "TimeStamp", title, reports);
    
        //GS - Ask a helper method to display this chart
        ViewHelper.DisplayChart(chart);
    }

    This method will firstly use a linq statement to get all the weather reports where the wind direction starts with the required direction: “N” for North; “S” for South; etc. The next thing it will do is call a helper method, passing in the chart title, the qualitative and quantitative properties of the Report object and the result of the linq query. Lastly, this method then calls another helper method in order to display the chart returned by the first helper method.

    Let’s have a look at this first helper method:

    public static ChartControl CreateBarChartFromLinqResult(
        string valueMember, 
        string dataMember, 
        string title, 
        IQueryable<Report> reports)
    {
        ChartControl chart = new ChartControl();
        chart.Titles.Add(new ChartTitle { Text = title });
        chart.Legend.Visible = false;
    
        //GS - Create an empty Bar series and add it to the chart.
        Series series = new Series(title, ViewType.Bar);
        chart.Series.Add(series);
    
        //GS - Supply the data
        series.DataSource = reports.ToList<Report>();
    
        //GS - Bind the data
        series.ArgumentScaleType = ScaleType.Qualitative;
        series.ArgumentDataMember = dataMember;
        series.ValueScaleType = ScaleType.Numerical;
        series.ValueDataMembers.AddRange(new string[] { valueMember });
    
        //GS - Dock the chart into its parent
        chart.Dock = DockStyle.Fill;
    
        //GS - Return the chart
        return chart;
    }

    This method is fairly simple, all it does is create a new chart object and add a series to it. We then attach our linq results as the data source and then tell the chart how to bind to that data. Finally, we set the dock style to fill and return the chart to the calling method.

    The second helper method is simpler still:

    public static void DisplayChart(ChartControl chart)
    {
        ChartViewer viewer = new ChartViewer();
        viewer.Controls.Add(chart);
        viewer.Show();
    }

    It takes in a chart object and instantiates a ChartViewer form before adding the chart to the form’s controls and then showing the form.

    So having explained how we do it, let’s have a look at the results, pressing the “North” button yields this chart:

    image

    The remaining “South”, “East” and “West” buttons are a repeat of the same pattern so there is no point in going over those.

    The next thing we are going to look at is charting the temperature fluctuations throughout the day. To do this we select a date and press the “Chart” button:

    image

    The event handler for the “Chart” button is shown below:

    private void bntChartWindSpeedByDay_Click(object sender, EventArgs e)
    {
        //GS - Get the selected data
        DateTime target = dateTimePicker1.Value;
        
        XPQuery<Report> reportQuery = new XPQuery<Report>(XpoDefault.Session);
    
        var reports = from r in reportQuery
                      where r.TimeStamp.Date == target.Date
                      select r;            
    
        //GS - If there is no matching data for the target date then we're done
        if (reports.Count<Report>() < 1)
            return;
    
        //GS - Ask a helper method to create a chart from this info
        ChartControl chart = ChartHelper.CreateBarChartFromLinqResult(
            "Temperature", "TimeStamp", 
            String.Format("Temperatures for: {0}", target.Date.ToLongDateString()), 
            reports);
    
        //GS - Ask a helper method to display this chart
        ViewHelper.DisplayChart(chart);
    }

    As you can see, we follow a similar pattern to that already discussed, whereby we execute a linq query against the XPO source, passing the result to a helper method which constructs and returns a chart object and we then pass that chart object to another helper function which displays the chart. The result of selecting a date and pressing the “Chart” button is shown below:

    image

    The last interesting piece of information that we are going to graph is the prevailing wind direction. The event handler for the “Show Prevailing Wind Direction” button is shown below:

    private void btnMeanWindDirection_Click(object sender, EventArgs e)
    {
        //GS - Fetch all the reports
        XPQuery<Report> reportQuery = new XPQuery<Report>(XpoDefault.Session);
    
        var reports = from r in reportQuery
                      select r;
    
        //GS - Ask a helper method to create a pie chart of the directionCounts
        ChartControl chart = ChartHelper.CreateWindDirectionPieChart(reports);
    
        //GS - Ask a helper method to display this chart
        ViewHelper.DisplayChart(chart);
    }  

    You will recognise this now familiar pattern of executing a linq statement and then passing the result to a helper method to construct and return a chart object before passing that chart object, in turn, to another helper method to display the chart. The only difference this time is the helper method which constructs the chart is different, let’s take a look at it now:

    public static ChartControl CreateWindDirectionPieChart(IQueryable<Report> reports)
    {
        ChartControl chart = new ChartControl();
        chart.Titles.Add(new ChartTitle { Text = "Prevailing Wind Direction" });
        chart.Legend.Visible = true;
    
        //GS - Create an empty Bar series and add it to the chart.
        Series series = new Series(String.Empty, ViewType.Pie3D);
        series.LegendPointOptions.PointView = PointView.ArgumentAndValues;
        series.PointOptions.ValueNumericOptions.Format = NumericFormat.Percent;
        chart.Series.Add(series);
    
        //GS - Supply the data
        List<WindDirectionCount> directionCounts = new List<WindDirectionCount>();
        directionCounts.Add(
            new WindDirectionCount
            {
                Direction = "North",
                Count = reports.Count<Report>(x => x.WindDirection.StartsWith("N"))
            });
    
        directionCounts.Add(
            new WindDirectionCount
            {
                Direction = "South",
                Count = reports.Count<Report>(x => x.WindDirection.StartsWith("S"))
            });
    
        directionCounts.Add(
            new WindDirectionCount
            {
                Direction = "East",
                Count = reports.Count<Report>(x => x.WindDirection.StartsWith("E"))
            });
    
        directionCounts.Add(
            new WindDirectionCount
            {
                Direction = "West",
                Count = reports.Count<Report>(x => x.WindDirection.StartsWith("W"))
            });
    
    
        series.DataSource = directionCounts;
    
        //GS - Bind the data
        series.ArgumentScaleType = ScaleType.Qualitative;
        series.ArgumentDataMember = "Direction";
        series.ValueScaleType = ScaleType.Numerical;
        series.ValueDataMembers.AddRange(new string[] { "Count" });
    
        //GS - Dock the chart into its parent
        chart.Dock = DockStyle.Fill;
    
        //GS - Return the chart
        return chart;
    }

    This helper method is similar, but a little different, to the previous one. Again it creates a chart object and adds a series to it. The series is a little different this time though:

    //GS - Create an empty Pie series and add it to the chart.
    Series series = new Series(String.Empty, ViewType.Pie3D);
    series.LegendPointOptions.PointView = PointView.ArgumentAndValues;
    series.PointOptions.ValueNumericOptions.Format = NumericFormat.Percent;
    chart.Series.Add(series);

    As you can see we are creating a pie chart this time and so we use the PointView.ArgumentAndValues enum to specify that we want to show the wind direction as well as the values and the NumericFormat.Percent enum to specify that we want to show the values as a percentage of the total.

    We then go on to count the number of weather reports that state the wind was from a particular direction (N, NNW all count as North etc.) and we go ahead and store this information in a list of objects we created for that purpose. These objects are very simple in nature and they are defined like so:

    using System;
    
    namespace XPOWeather2
    {
        public class WindDirectionCount
        {
            public string Direction { get; set; }
            public int Count { get; set; }        
        }
    }

    Then we set the data and bind to is as before, specifying the qualitative and the quantitative properties, before finishing by asking the same helper method to display the chart. The result of all this is shown below:

    image

    As you can see, the prevailing wind direction in this part of the world is shown as westerly (which it actually is!).

    Well that about wraps it up for this post, ‘til the next time, happy XPOing!

  • XPO – Charting Local Weather Conditions #1

    All the talk of climate change means that there are a lot of people interested in the weather right now, and rightly so. In the first of two posts on this topic, I’m going to show you how to sample weather data, on on hourly basis, and then use XPO to store that data. In the second post we will look at charting that data. But first, how do you get weather information if you don’t have a weather station in your garden? Well, the answer is, you can use other people’s data.

    You may not know this, but airports, military basis and other weather stations report weather conditions on an hourly basis and this information is held by the National Oceanic and Atmospheric Administration and is made available to the public. This system is known as METAR. It is this system we will make use of to sample the weather reports that we need.

    The weather reports are filed under the ICAO code for the station they come from, so the first thing we need to do is to identify which station we are going to use and what the code is for that station. I live only a few miles from Dundee Airport and so I’m going to use that for my reports. I need to find the ICAO code for Dundee Airport and a simple Google search tells me the code is EGPN.

    Next we need to know where to retrieve the report from. METAR reports are encoded and are, frankly, a pain in the bum to decode. Luckily for us, those nice people at the NOAA have taken care of that for us. They file decoded METAR reports at: ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/. All we have to do is to pull back the text file EGPN.txt from that location, parse it and store that information. So let’s go ahead now and build an application to do that.

    Create a standard console application:

    image

    Now let’s create a METAR helper class with a function to fetch the report for us:

    public static void GetMetarDataForCode(string code)
    {
        if (String.IsNullOrEmpty(code))
            return;
    
        const string ADDRESS =
            @"ftp://tgftp.nws.noaa.gov/data/observations/metar/decoded/";
    
        string url = String.Format("{0}{1}.TXT", ADDRESS, code.ToUpper());
    
        string report;
    
        try
        {
            report = new WebClient().DownloadString(url);
            StoreMetarData(report);
        }
    
        catch (WebException wex)
        {
            Logger.Log(String.Format("{0}: {1}", DateTime.Now, wex.Message));
        }           
    }

    Which we’ll call from our program entry point:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace XPOWeather1
    {
        class Program
        {
            static void Main(string[] args)
            {
                const string METAR_CODE = "EGPN";
                MetarHelper.GetMetarDataForCode(METAR_CODE);
            }
        }
    }

    Next, let’s define an model class to hold our report. As this class extends XPObject we’ll need to add references to DevExpress.Data and DevExpress.Xpo at this point. We are not interested in all of the data in the report, so something like this should suffice:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using DevExpress.Xpo;
    
    namespace XPOWeather1.Model
    {
        public class Report: XPObject
        {
            public Report(Session session)
                : base(session)
            { }
    
            private DateTime timeStamp;
            public DateTime TimeStamp
            {
                get
                {
                    return timeStamp;
                }
                set
                {
                    SetPropertyValue("TimeStamp", ref timeStamp, value);
                }
            }
    
            private string skyConditions;
            public string SkyConditions
            {
                get
                {
                    return skyConditions;
                }
                set
                {
                    SetPropertyValue("SkyConditions", ref skyConditions, value);
                }
            }
    
            private string visibility;
            public string Visibility
            {
                get
                {
                    return visibility;
                }
                set
                {
                    SetPropertyValue("Visibility", ref visibility, value);
                }
            }
    
            private string windDirection;
            public string WindDirection
            {
                get
                {
                    return windDirection;
                }
                set
                {
                    SetPropertyValue("WindDirection", ref windDirection, value);
                }
            }
    
            private int windSpeed;
            public int WindSpeed
            {
                get
                {
                    return windSpeed;
                }
                set
                {
                    SetPropertyValue("WindSpeed", ref windSpeed, value);
                }
            }
    
            private int temperature;
            public int Temperature
            {
                get
                {
                    return temperature;
                }
                set
                {
                    SetPropertyValue("Temperature", ref temperature, value);
                }
            }
        }
    }

    Having done that, we can write the function to create and store the Report:

    private static void StoreMetarData(string reportText)
    {
        if (String.IsNullOrEmpty(reportText))
            return;
    
        using (UnitOfWork uow = new UnitOfWork())
        {
            Report report = new Report(uow) 
            { 
                SkyConditions = GetSkyConditionsFromReportText(reportText), 
                Temperature = GetTemperatureFromReportText(reportText), 
                TimeStamp = GetTimeStampFromReportText(reportText), 
                Visibility = GetVisibilityFromReportText(reportText), 
                WindDirection = GetWindDirectionFromReportText(reportText), 
                WindSpeed = GetWindSpeedFromReportText(reportText) 
            };
    
            uow.CommitChanges();
        }
    }

    Here are the definitions for those helper functions, just for completeness sake:

    private static string GetSkyConditionsFromReportText(string reportText)
    {
        string result = String.Empty;
        if (String.IsNullOrEmpty(reportText))
            return result;
    
        return GetInformationFromReportText("Sky conditions:", reportText);
    }
    
    private static string GetInformationFromReportText(string key, string reportText)
    {
        string result = String.Empty;
        if (String.IsNullOrEmpty(key) || String.IsNullOrEmpty(reportText))
            return result;
        
        return reportText.Split(Environment.NewLine.ToCharArray()).
            First<string>(x => x.Contains(key)).
                Split(":".ToCharArray())[1].Trim();
    }
    
    private static int GetTemperatureFromReportText(string reportText)
    {
        if (String.IsNullOrEmpty(reportText))
            return 0;
    
        return Convert.ToInt32(GetInformationFromReportText(
            "Temperature:", reportText).Split("(".ToCharArray())[1].
                Split(")".ToCharArray())[0].Split(" ".ToCharArray())[0]);
    }
    
    private static DateTime GetTimeStampFromReportText(string reportText)
    {
        DateTime result = DateTime.MinValue;
        if (String.IsNullOrEmpty(reportText))
            return result;
    
        string temp = reportText.Split(Environment.NewLine.ToCharArray()).
            First<string>(x => x.Contains("UTC")).Split("/".
                ToCharArray())[1].Replace(".", "/");
    
        string temp2 = temp.Substring(0, temp.Length - 6);
        string temp3 = temp.Substring(temp.Length - 6);
        string temp4 = String.Format("{0}:{1}", temp2, temp3);
    
        return DateTime.Parse(temp4.Remove(temp4.Length - 3, 3).Trim());
    }
    
    private static string GetVisibilityFromReportText(string reportText)
    {
        string result = String.Empty;
        if (String.IsNullOrEmpty(reportText))
            return result;
    
        return GetInformationFromReportText("Visibility:", reportText);
    }
    
    private static string GetWindDirectionFromReportText(string reportText)
    {
        string result = String.Empty;
        if (String.IsNullOrEmpty(reportText))
            return result;
    
        return reportText.Split(Environment.NewLine.ToCharArray()).
            First<string>(x => x.StartsWith("Wind:")).Split(":".
                ToCharArray())[1].Split("(".ToCharArray())[0].Trim().
                    Split(" ".ToCharArray())[2];
    }
    
    private static int GetWindSpeedFromReportText(string reportText)
    {
        if (String.IsNullOrEmpty(reportText))
            return 0;
    
        return Convert.ToInt32(reportText.Split(Environment.NewLine.ToCharArray()).
            First<string>(x => x.StartsWith("Wind:")).Split(")".
                ToCharArray())[1].Trim().Split(" ".ToCharArray())[1]);
    }

    The last thing we have to do in this simple little application is to create a Logger helper class and provide a Log helper function, so we can log any web exceptions as we’re going to run this unattended:

    using System;
    using System.IO;
    
    namespace XPOWeather1
    {
        public class Logger
        {
            public static void Log(string message)
            {
                File.AppendAllText(@".\Files\Log.txt", message);
            }
            
        }
    }

    Now that we are finished, our project looks like this:

    image

    Running this application gives us a nice record of the weather, updated by the Dundee Airport weather station on an hourly basis:

    image

    The last thing we need to do in this post is to set up our application to run hourly and pick up the weather reports. We’ll do this by using the Task Scheduler:

    image

    In the next post, we’ll look at retrieving this information and charting some important weather information. Until then… happy XPOing!

  • Cloud Computing Could Save Your Life…

    Well okay maybe not your life, because let’s face it, you are probably a well paid programmer living in the developed world. But watching the stories of the earthquake in Sumatra and the tsunami in Samoa over the last few days got me thinking that cloud computing could be a real life saver in these situations. How so? Well, did you know that an estimated $6.2 billion of donated medical supplies and 96 billion pounds of food never make it to the people who need them each year, simply because the technology and skills required for effective distribution are not available?

    All the experts will tell you that the faster you get medical supplies and general “aid” to the disaster stricken regions then the more lives you can save. There will also be disaster relief plans set up ready to be put into action at a moment’s notice. These plans, I’m sure, are very detailed and cover every foreseeable eventuality. Of course, any soldier will tell you that it doesn’t matter how good your planning is, no plan survives first contact with the enemy. The enemy in this case is going to be the disaster itself. As soon as relief workers arrive on the scene, “the plan” is going to have to be changed to fit the particular needs of the survivors. Systems are going to have to be in place  to handle things like causality bureaux, missing persons, food/water distribution and other general logistical requirements. All this information is going to have to be fed back to HQ so that “the plan” can be amended as required.

    All of this is perfectly obvious. The trouble is that… well the place is a disaster area, right? That’s why the relief workers are there in the first place. That causes two problems. Firstly, the lack of power on scene to run these systems and two, the relief agencies would have to ship all this kit out to the location, when what they really need to be doing is shipping food/medicine/rescue equipment etc. This is where cloud computing comes in. With all of the databases and “heavy computing” being abstracted out to the cloud, it now becomes possible for the relief workers to use much smaller computers – perhaps even PDAs which they can fit in their pockets, or in their back packs.

    Now there isn’t likely to be any power on scene, but that is not an issue as they will be shipping generators anyway to power the lights and other rescue equipment so that’s okay. The other issue of course is connectivity, a cloud based solution is no use if you can’t connect to it. There are ways to overcome this problem however. If there are 3G towers in the area, and they have survived, then you can use products like this one to connect to them without a hotspot. If there is no network left then you can use a portable satellite solution, these things are not particularly big or heavy to ship.

    Taking things a step further, instead of shipping the computers, wouldn’t it be cool if they could just ship the software instead? Well there is no reason why they couldn’t do that too. All they’d have to do is to write these systems for Linux – using Mono for example – then spin a LiveCD version of Linux, including the client systems. These respins could be loaded onto CD or USB sticks and carried to the scene in the pockets of rescue workers. Once on scene, they’d need to find a building with power and connectivity, but if they could – or if they knew of such a building before they left – they could just plug in the usb sticks, power up the machines and away they’d go. Once they were finished, just power down the machines again and then it’s as if they’d never been there. 

    Okay, so I’m sure there are lots of little niggly issues around my great plan, but I’m sure they could be resolved if enough thought was put to them. People are realising that software is required at the scene of these disasters. After hurricane Katrina the Department of Homeland Security recommended that a Chief Logistics Officer be appointed. Part of his remit is to procure software of this kind and a cloud based solution would appear, to me, to be the obvious one. What do you think?

  • XAF – Project Management Application #10

    Phew, well I’m back from a hectic week at BASTA and all set to post the latest in our series on creating an project management application using XAF. So, what are we going to do in this post? Well, first of all our status changing algorithm isn’t very good is it? For a start you can’t go backwards to a previous status and it assumes that their is only one possible status to go forward to. These two things make our model of status pretty unrealistic. So, let’s do something about that.

    A state machine or workflow solution sounds best suited to fixing this problem and, as I’ve said, there are plans for such things, but for now we’ll allow users to decide which status suits best. We’ll get rid of the “Change status” button and make the status list editable. To do this, we’ll remove the “protected” keyword from the TaskBase’s Status property setter declaration.

    Now we’re able to change the status manually:

    1

     2

    Okay, well that sort of worked, but there are a couple of issues:

    1. The New and Clear buttons are displayed in the lookup.
    2. Only the statuses corresponding to the current task type should be displayed, not all of them.
    3. Statuses are sorted in the alphabetical order instead of their logical sequence;

    So, let’s write a test for the New button to ensure it’s unavailable:

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Project Tasks)

    *Action New

    #IfDef XProjectWin

    *ExecuteEditorAction Status

    !ActionAvailable New

    #EndIf

    #IfDef XProjectWeb

    !ExecuteEditorAction Status(Add)

    #EndIf

    Looking at this test, you can see that the syntax is different between the web and winforms actions; oops. :-) Don’t worry we are aware of this and will fix it. Running that test at the moment fails, not surprisingly. To remove the New button we’ll create a new AvailableStatuses property on the TaskBase Class:

    public abstract class TaskBase : BaseObject {
    …
        private XPCollection<TaskStatus> availableStatuses;
    
        [Browsable(false)]
        public XPCollection<TaskStatus> AvailableStatuses {
            get {
                if(availableStatuses == null) {
                    availableStatuses = new XPCollection<TaskStatus>(StatusSet.Items);
                    availableStatuses.BindingBehavior = CollectionBindingBehavior.AllowNone;
                }
    
                return availableStatuses;
            }
        }
    …
    }

    Check out the [Browsable(false)] attribute above, using this means that the property will not be visible in the UI. Meantime, the AvailableStatuses field will hold a local status collection. We’ll set this property’s BindingBehavior to CollectionBindingBehavior.AllowNone, so that the New button won’t be available in the AvailableStatuses lookup. Also note that the availableStatuses field is initialized only once when its value is first accessed. Now let’s limit the statuses displayed in the lookup to those that belong to the current task type; to do that we apply the DataSourceProperty("AvailableStatuses") attribute to the Status property:

    [Persistent, DataSourceProperty("AvailableStatuses")]
    public TaskStatus Status {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        set { SetPropertyValue<TaskStatus>("Status", value); }
    }

    Launching the application, we see that the “new” button has gone and the status list looks much better:

    3

    4

    Running the functional test, above, now passes as a result of this change. :-)

    The next thing we are going to do is to nail that “Clear” button problem, we need to remove it so that the Status can’t be set to NULL. We’ll also write a unit test to check this:

    [Test]
    public void Status_CannotAssignNull() {
        TestProjectTaskClass task = new TestProjectTaskClass(Session);
        Assert.IsNotNull(task.Status);
    
       try {
            task.Status = null;
            Assert.Fail();
        }
        catch (ArgumentNullException){ }
    }

    No surprise, the test fails. So let’s modify the Status property a little:

    public TaskStatus Status
    {
        get { return GetPropertyValue<TaskStatus>("Status"); }
        set
        {
    
            if (!IsLoading)
                Guard.ArgumentNotNull(value, "Status");
    
            SetPropertyValue<TaskStatus>("Status", value);
        }
    }

    Running the test again shows it passes. Ah, don’t you just love the little victories? :-)

    Now we need to remove the “Clear” button from the Windows Forms application. To do this we’ll add a new TaskStatusLookupController View Controller. We want this Controller to be activated only for the Status lookup and so we’ll set its TargetViewId property to TaskStatus_LookupListView. We’ll write the code to remove the “Clear” button in the Controller’s OnActivated method:

    public class TaskStatusLookupController : ViewController
    {
        public TaskStatusLookupController()
        {
            TargetViewId = "TaskStatus_LookupListView";
        }
    
        protected override void OnActivated()
        {
            base.OnActivated();
    
            LookupWindowController standardController = Frame.GetController<LookupWindowController>();
            if (standardController != null)
                standardController.Active.SetItemValue("TaskStatusLookupController", false);
        }
    }

    As you can see, the “Clear” button is now gone:

    5

    Now, let’s go ahead and remove the “Clear” button from the ASP.Net application. Umm… yeah… Turns out there’s no easy way to do this at the moment, so we’ll create a suggestion for the XAF team to write such functionality, and quietly move on. :-)

    Moving on… let’s fix the issue of the alphabetical order of the statuses. We’ll replace our enumerations with string arrays that contain statuses in the required order and we’ll modify the UpdateDatabase method in the TaskBase class to achieve this:

    protected static void UpdateDatabase(Session session, string[] statusNames, string statusSetName)
    {
        TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
    
        if (statusSet == null)
        {
            TaskStatus[] statusList = new TaskStatus[statusNames.Length];
            statusSet = new TaskStatusSet(session, statusSetName);
    
    
            TaskStatus nextStatus = null;
            for (int j = statusNames.Length - 1; j >= 0; j--)
            {
                string statusName = statusNames[j];
                statusList[j] = new TaskStatus(session, statusSet, nextStatus, statusName);
                nextStatus = statusList[j];
            }
    
            statusSet.FirstStatus = statusList[0];
            statusSet.LastStatus = statusList[statusList.Length - 1];
    
            statusSet.Save();
        }
    }

    We also need to change the initialisation code of our Task:

    public class ProjectTask : TaskBase {
        public static readonly string[] InitialStatusNames = new string[] { "Draft", 
            "NotStarted", 
            "InProgress", 
            "Paused", 
            "Completed" };
    …
        public new static void UpdateDatabase(Session session) {
            UpdateDatabase(session, InitialStatusNames, StatusSetName);
        }
    …
    }

    Having made this change, I’ll leave you to remove the enumerations and amend the unit tests accordingly. Also, we need to amend the AvailableValuesForTaskStatus functional test. Change:

    !FillForm

    Status = Completed

    To:

    *FillForm

    Status = Completed

    To change the sort order of the statuses in the lookup we’ll add an Index property to the TaskStatus class. Since we are going to use indexes instead of an ordered list, we’ll remove the Next property. In the TaskStatus constructor we’ll replace the nextStatus parameter with the index parameter:

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

    We’ll also initialize the new field in the TaskBase class’ UpdateDatabase method:

    protected static void UpdateDatabase(Session session, string[] statusNames, string statusSetName)
    {
        TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
    
        if (statusSet == null)
        {
            TaskStatus[] statusList = new TaskStatus[statusNames.Length];
            statusSet = new TaskStatusSet(session, statusSetName);
    
    
            for (int j = 0; j < statusNames.Length; j++)
            {
                statusList[j] = new TaskStatus(session, statusSet, j, statusNames[j]);
            }
    
            statusSet.FirstStatus = statusList[0];
            statusSet.LastStatus = statusList[statusList.Length - 1];
    
            statusSet.Save();
        }
    }

    Now we must get rid of the NextStatus button and the TaskBase class’ SetNextStatus method. We’ll remove this method as well as the ChangeTaskStatusController Controller and we’ll remove some unit tests which are now useless. For this purpose we’ll update and move the correct status order checking logic from the descendant tasks tests to the TaskBase tests:

    [Test]
    public void UpdateDatabase_CorrectStatusSequence()
    {
        TaskStatusSet statusSet = TestProjectTaskClass.FindTaskStatusSet(Session, TestProjectTaskClass.StatusSetName);
        Assert.IsNotNull(statusSet);
    
        List<string> statusList = new List<string>(TestProjectTaskClass.InitialStatusNames);
        foreach (TaskStatus currentStatus in statusSet.Items)
        {
            Assert.AreEqual(statusList.IndexOf(currentStatus.Name), currentStatus.Index);
        }
    }

    Now we’ll configure the order of the items in the lookup editor for the status property. In the Model Editor open the TaskStatus_LookupListView node and add a new column for the Index property, disable sorting for the Name property and enable sorting for the Index property (set the required value for the SortOrder attribute):

    6

    Right, so let’s fire up our applications now:

    7

     8

    Yay! The winform application looks great and the webform application… meh, not so much. Looks like we’ve found a bug. The list of objects in Simple mode (ComboBox edit) is always sorted by Name regardless of what the settings in ListView node are.

    Well, while that bug is getting fixed we’ll go ahead and introduce the capability to modify/edit status sets from the UI. First, we’ll apply the [NavigationItem("Settings")] attribute to the TaskStatusSet class this will mean that we can now edit the StatusSet objects in the UI:

    9

     10

    Hmm, well it’s a start but there are still issues: the Name property is editable, which can cause a mismatch between a task type and the corresponding status set. To fix this, we’ll make the TaskStatusSet’s Name property setter protected. Additionally we’ll apply the Persistent attribute, so that XPO will persist this property:

    [Persistent]
    public string Name
    {
        get { return GetPropertyValue<string>("Name"); }
        protected set { SetPropertyValue<string>("Name", value); }
    }

    Also, the “Task Status Set” caption in the navigation control isn’t very useful. We’ll rename it “Task Statuses”. To do this, add the DisplayName attribute to the TaskStatusSet class:

    [NavigationItem("Settings"), System.ComponentModel.DisplayName("Task Statuses")]
    public class TaskStatusSet : BaseObject
    {
    …
    }

    Next, the default layout of controls isn’t very convenient. For example, the Name property should be displayed first. We’ll use the Model Editor to fix this. Double click the Model.DesignedDiffs.xafml file in the module project, navigate to the Application\Views\TaskStatusSet_DetailView\Layout node. Right click the design surface and choose “Customize Layout”:

    11

    Statuses should be sorted by their indexes. To configure the sort order we’ll use the Model Editor (again <grin>). If we navigate to the TaskStatusSet_Items_ListView node, we’ll see that by default the Name property is used for sorting purposes (as it’s the default property). We want to sort by the Index property. To achieve this, change the SortIndex and SortOrder attributes’ values for the Name and Index properties. To check the default sorting we’ve written a functional test - StatusSet_Items_DefaultSortByIndex.ets which now passes:

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Task Statuses)

    *ProcessRecord

    Name = ProjectTask

    *CheckTable Items

    Columns = Index

    Row[1] = 0

    Row[2] = 1

    Row[3] = 2

    Row[4] = 3

    Row[5] = 4

    Users shouldn’t be able to create/delete StatusSets, we should only allow editing.Again we invoke the Model Editor and set the AllowNew and AllowDelete attribute to False for the TaskStatusSet_ListView and TaskStatusSet_DetailView Views. And again we write a functional test to check that the New and Delete Actions are disabled (StatusSet_DisabledNewDeleteActions.ets script file):

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Task Statuses)

    !ProcessRecord

    Name = ProjectTask

    Action = Delete

    !ActionAvailable TaskStatusSet.New

    ;using ProjectTask's existing statuses

    *ProcessRecord

    Name = ProjectTask

    !ActionAvailable TaskStatusSet.Delete

    We still need to filter the drop-down lookups for the FirstStatus and LastStatus properties. To do this we’ll apply the [DataSourceProperty("Items")] attribute to both properties of the TaskStatusSet class:

    public class TaskStatusSet : BaseObject
    {
    …
    
        [DataSourceProperty("Items")]
        public TaskStatus FirstStatus {
            get { return GetPropertyValue<TaskStatus>("FirstStatus"); }
            set { SetPropertyValue<TaskStatus>("FirstStatus", value); }
        }
    
        [DataSourceProperty("Items")]
        public TaskStatus LastStatus {
            get { return GetPropertyValue<TaskStatus>("LastStatus"); }
            set { SetPropertyValue<TaskStatus>("LastStatus", value); }
        }
    }
    Phew, well I don’t know about you, but I’ve had quite enough for this post. If you want to play along at home, you can download the code from here. Next time, we’ll integrating his application with another internal application, in the meantime, if you are doing anything interesting with XAF or XPO drop me a line and tell me all about it and… happy XAFing!
  • Sorry I’m not Here Right Now, Please Leave a Message After the Tone…

    As you may have guessed from the title, I’m not around right now. In fact I’ll be exhibiting the DevExpress product line at the BASTA conference in Mainz, Germany. If you are going to be attending the conference please stop by the booth and say hello, it’s always nice to meet readers and “put a face to a name”. If you are not going to be in Germany for the conference, then you can keep up to date with what is going on there by following me on Twitter (@garyshort).

    Normal service, well as normal as it gets around here anyway, will be resumed next Monday.

  • Successful Companies Follow Their Customers

    By 2010 the baby boomers will have given way to Generation Y and so your target marketing demographic will have moved on to a generation who are far more au fait with social technology than the generation that preceded them (96% of them have joined a social network). What will that mean for your future marketing strategies? Well for one thing, you are going to have to move your marketing online, if you haven’t already. Social networking has overtaken pornography as the #1 activity on the Internet, that’s where your customers are and that’s where you have to be too.

    Not only are your customers moving online, but the rate at which technology is reaching critical mass is increasing: whist it took radio 38 years to gain 50 million users, Facebook gained 100 Million users in less than 8 months and, incredibly, Apple gained 1 billion downloads from its AppStore in just 9 months. You may not think your company/product needs to be on Facebook, but with its 300 million active users, if it were a country, it would be the fourth largest; why would you want to shut yourself off from a market of that size?

    Meh, I hear you say, “we’re already online, we have a blog, Google knows where we are and therefore our customers know where we are” Really? Well that might be true, but did you also know that YouTube is the second largest search engine in the world? You have to think beyond producing the same bland, text based content as your competitors; you must start to innovate with other media, like video for example, to get your message across. Watching video content online is popular; Hulu, for, example, has grown from 63 million streams to 373 million in 1 year.

    Of course, when I say you must move your marketing online in order to reach the next generation of the target advertising demographic, I don’t just mean advertise online. Why not, I hear you ask? Simple, no one believes online advertising. Well, okay, that might be a bit of an exaggeration; the fact is only 33% of consumers trust online banner ads, versus 90% who trust word of mouth recommendations from friends. So the message is clear, if you want to sell to the next generation you have to talk to them.

    Talk to them? But when? Where? Well the answer to that is simple: anytime and anywhere. 17% of Brits and 15% of Americans now social network via mobile phone your customers are never more than a few key presses away from their blogs; think about that the next time one of your staff members upsets a customer. There are nearly 200,000,000 blogs on the Internet with 54% of bloggers posting daily and 26% of bloggers blogging about products or brands. That means there is someone talking about your products. The time for deciding if you want your product to be talked about has gone – the only thing left to decide is: do you want to try and shape that conversation?

    Still not convinced that social media is the future of advertising and therefore the future of your company? What about print advertising I hear you say, that still works… doesn’t it? Not so much. Newspaper readership fell from 34% to 25% between 2006 and 2009 and the trend lines have been heading south for years. The truth is the smart money has moved online.

    Okay, so I’ve convinced you that you have to at least “dip your toe in the water” of social marketing, the question now is: how do you get started? Well, there are some great books here, here and here. Read them, put the lessons learned into practice and you’ll not go too far wrong. You should also check out these great blogs:

    http://sethgodin.typepad.com/
    http://www.chrisbrogan.com/
    http://www.steverubel.com/
    http://blog.guykawasaki.com/
    http://www.buzzmachine.com/

    But in the end, the best advice I can offer you is to get out there and do it, be honest, and just talk to your customers; they’re already talking about about you. :-)

  • XAF – Project Management Application #9

    In this edition of our continuing series of blog posts on how to create a project management application in XAF, we will be progressing with the statuses functionality within our application. This time around, we’ll be adding a new task type, cleaning up our code a little bit and adding some logic around the rules of marking a task as complete.

    The first thing we are going to do is to add a new engineering task type, with it’s own status set:

    [DefaultClassOptions]
    [NavigationItem("Planning")]
    public class EngineeringTask : BaseObject {
        public EngineeringTask(Session session) : base(session){     }
    }
    public enum EngineeringTaskStatus {
        Draft,
        Planned,
        Elaboration,
        Development,
        Documenting,
        Completed
    }

    Now that we have both a ProjectTask and an Engineering task, it stands to reason that there is going to be duplicate code, i.e. code that appears both in the ProjectTask and the EngineeringTask. The duplication of code can only lead to errors at maintenance time, and so we should create a common base class, from which each class can derive and place all the common code there, leaving the subclasses to house the specific task code:

    public abstract class TaskBase : BaseObject
    {
        public TaskBase(Session session)
            : base(session)
        {
        }
    }

    Now let’s move the common code from the ProjectTask into the TaskBase class:

    [RuleCriteria("ProjectTask-EndDateMustBeAfterStartDate", DefaultContexts.Save, "EndDate > StartDate")]
    public abstract class TaskBase : BaseObject {
        public const string StatusSetName = "TaskBase";
    
        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;
        }
        protected void col_CollectionChanged(object sender, XPCollectionChangedEventArgs e) {
            if(e.CollectionChangedType == XPCollectionChangedType.BeforeRemove) {
                RemainingHours += ((TaskWork)e.ChangedObject).HoursSpent;
                UpdateHoursSpent();
            }
        }
        protected abstract string GetStatusSetName();
        protected static void UpdateDatabase<EnumerationType>(Session session, string statusSetName) {
            if(!typeof(EnumerationType).IsEnum)
                throw new ArgumentOutOfRangeException();
    
            TaskStatusSet statusSet = FindTaskStatusSet(session, statusSetName);
    
            if(statusSet == null) {
                string[] statusNames = Enum.GetNames(typeof(EnumerationType));
                TaskStatus[] statusList = new TaskStatus[statusNames.Length];
                statusSet = new TaskStatusSet(session, statusSetName);
    
    
                TaskStatus nextStatus = null;
                for(int j = statusNames.Length - 1; j >= 0; j--) {
                    string statusName = statusNames[j];
                    statusList[j] = new TaskStatus(session, statusSet, nextStatus, statusName);
                    nextStatus = statusList[j];
                }
    
                statusSet.FirstStatus = statusList[0];
                statusSet.LastStatus = statusList[statusList.Length - 1];
    
                statusSet.Save();
            }
        }
    
        public TaskBase(Session session)
            : base(session) {
        }
        public override void AfterConstruction() {
            base.AfterConstruction();
    
            this.StatusSet = FindTaskStatusSet(Session, GetStatusSetName());
            this.Status = this.StatusSet.FirstStatus;
        }
        public static void UpdateDatabase(Session session)
        {
            throw new NotImplementedException("Inheritor class has to override (witn 'new' keyword) and implement this method");
        }
        public static TaskStatusSet FindTaskStatusSet(Session session, string statusSetName) {
            return session.FindObject<TaskStatusSet>(new BinaryOperator("Name", statusSetName));
        }
        public void SetNextStatus() {
            if(this.Status == this.StatusSet.LastStatus) {
                throw new InvalidOperationException();
            }
    
            this.Status = this.Status.NextStatus;
        }
    
        //properties (unchanged)
       ...
    }

    So let’s see what it was that we did here. We didn’t apply the “DefaultClassOptions” and “NavigationItem("Planning")” attributes because the class cannot be instantiated and so shouldn’t have a navigation item. We’ve also introduced a new constant (StatusSetName) which identifies the status set corresponding to the current task type, descendants of this class will have to override this to specify their own StatusSet. Then, We defined a new function – UpdateDatabase, which will be used by TaskBase descendants to set the required status set types. As you can see to make sure that this method cannot be used unless overridden, we throw an exception in it. Having done that, we’ve marked the old UpdateDatabase method as protected and made it generic. The generic type parameter specifies the required status set. In the method we also check that the passed type is an enumeration. Also, the FindTaskStatusSet method now takes an additional parameter which specifies the status set name. We’ve added this new status set name parameter to the FindTaskStatusSet and UpdateDatabase methods because these method implementations reside in the base class, while the methods are going to be called from descendants which can use different status sets.  To move the AfterConstruction method to the base class we’ve replaced the call to the StatusSetName property with a call to the GetStatusSetName method. This method is abstract – so, descendants have to implement it.

    Now that we’ve defined our TaskBase class, let’s go back and define our new ProjectTask class:

    [DefaultClassOptions]
    [NavigationItem("Planning")]
    public class ProjectTask : TaskBase
    {
        public new const string StatusSetName = "ProjectTask";
    
        public ProjectTask(Session session)
            : base(session)
        {
        }
        public new static void UpdateDatabase(Session session)
        {
            UpdateDatabase<ProjectTaskStatus>(session, StatusSetName);
        }
        protected override string GetStatusSetName()
        {
            return StatusSetName;
        }
    }
    
    public enum ProjectTaskStatus
    {
        Draft,
        NotStarted,
        InProgress,
        Paused,
        Completed
    }

    Looking at this code we can see what we have to do when we derive a new task type:

    1. Override the StatusSetName constant
    2. Implement the GetStatusSetName method
    3. Override the the public UpdateDatabase method to call the base protected UpdateDatabase method.

    Now that we know what needs to be done, let’s go ahead and define our EngineeringTask class:

    [DefaultClassOptions]
    [NavigationItem("Planning")]
    public class EngineeringTask : TaskBase
    {
        public new const string StatusSetName = "EngineeringTask";
    
        public EngineeringTask(Session session)
            : base(session)
        {
        }
    
        public new static void UpdateDatabase(Session session)
        {
            UpdateDatabase<EngineeringTaskStatus>(session, StatusSetName);
        }
    
        protected override string GetStatusSetName()
        {
            return StatusSetName;
        }
    }
    
    public enum EngineeringTaskStatus
    {
        Draft,
        Planned,
        Elaboration,
        Development,
        Documenting,
        Completed
    }

    Oh, and we also have to change all the existing ProjectTask class references to the TaskBase class. If we now launch the application, we’ll be able to create a EngineeringTask object and see that it has all the fields of the TaskBase class, but the status set is differently (you can see the “Elaboration” status on the screenshot).

    1

      2

    One thing though, most of the fields are declared in the TaskBase class, and so they are displayed in the “Task Base” (and not the “Engineering Task”) layout group. This looks a bit odd, so let’s correct it. To do that we’ll need to edit the Application Model for the module project. We’ll rename this layout group for the EngineeringTask and ProjectTask classes:

    <DetailView ID="EngineeringTask_DetailView">
      <Layout>
        <LayoutGroup ID="Main">
          <LayoutGroup ID="SimpleEditors">
            <LayoutGroup ID="TaskBase" Caption="Engineering Task" />
          </LayoutGroup>
        </LayoutGroup>
      </Layout>
    </DetailView>
    <DetailView ID="ProjectTask_DetailView">
      <Layout>
        <LayoutGroup ID="Main">
          <LayoutGroup ID="SimpleEditors">
            <LayoutGroup ID="TaskBase" Caption="Project Task" />
          </LayoutGroup>
        </LayoutGroup>
      </Layout>
    </DetailView>

    Now, that looks a bit better, doesn’t it?

    3

     4

    Okay, so the TaskBase class is abstract but we’ve still got to test it and to do that, we’re going to need a tests subclass. To create one we’ll create a new TaskBaseTests class and copy the ProjectTaskTests’s SetUp method to the TaskBaseTests, and change ProjectTask.UpdateDatabase(Session) to TestProjectTaskClass.UpdateDatabase(Session). Then move all the common functionality tests, changing the ProjectTask class name to TestProjectTaskClass and ProjectTaskStatus enumeration to TestProjectTaskStatus enumeration.

    internal class TestProjectTaskClass : TaskBase
    {
        public new const string StatusSetName = "TestProjectClass";
    
        public TestProjectTaskClass(Session session)
            : base(session)
        {
        }
    
        public new static void UpdateDatabase(Session session)
        {
            UpdateDatabase<TestProjectTaskStatus>(session, StatusSetName);
        }
    
        protected override string GetStatusSetName()
        {
            return StatusSetName;
        }
    }
    
    public enum TestProjectTaskStatus
    {
        New,
        InProgress,
        Complete
    }

    Now that we moved all the common functionality tests to the TaskBaseTests class, we can modify the ProjectTaskTests class:

    [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);
    }
    
    [Test]
    public void UpdateDatabase_CorrectStatusSequence() {
        TaskStatusSet statusSet = ProjectTask.FindTaskStatusSet(Session, ProjectTask.StatusSetName);
        Assert.IsNotNull(statusSet);
    
        TaskStatus currentStatus = statusSet.FirstStatus;
        Assert.AreEqual(ProjectTaskStatus.Draft.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatus.NotStarted.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatus.InProgress.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatus.Paused.ToString(), currentStatus.Name);
    
        currentStatus = currentStatus.NextStatus;
        Assert.AreEqual(ProjectTaskStatus.Completed.ToString(), currentStatus.Name);
    
        Assert.AreEqual(null, currentStatus.NextStatus);
    }

    In the same way, we’d also create a test class for the EngineeringTask Class.

    In the previous post we’d written an EasyTest script to ensure that the ChangeStatus button is inactive for completed tasks and active for the incomplete. It turns out that sometimes the button’s state doesn’t correspond to the current task’s status, so we need an additional test:

    #DropDB XProjectEasyTest

    #Application XProjectWin

    #Application XProjectWeb

    *Action Navigation(Project Tasks)

    *Action Filter(All Tasks)

    *ProcessRecord

    Name = CompletedTask

    !ActionAvailable Change Status

    #IfDef XProjectWin

    *Action Close

    #EndIf

    *Action Navigation(Project Tasks)

    *ProcessRecord

    Name = DraftTask

    *ActionAvailable Change Status

    Now if we run it we’ll see that it fails:

    Application: XprojectWin Error in test: Action: 'Change Status' is available, line 13

    Let’s take a closer look at the ChangeTaskStatusController’s code. Set a breakpoint inside the OnActivated and View_CurrentObjectChanged methods. Then launch the application and navigate between various tasks. After that try to open a task which has the “Completed” status. We can see that when a Detail View is open the View_CurrentObjectChanged event doesn’t fire. Because of this the UpdateChangeStatusActionState method isn’t called and the button remains active. Bummer.

    To fix this, we’ll get rid of the CanChangeStatusController.UpdateChangeStatusActionState method  and instead we’ll use the Action.TargetObjectsCriteria property:

    public class ChangeTaskStatusController : ViewController {
        public ChangeTaskStatusController() {
                      …  
            changeTaskStatusAction.TargetObjectsCriteria = "CanChangeStatus";
           …
        }
    …
    }

    Now running the same test again shows that we have fixed the problem.

    The last thing we are going to do in this post is to code up the logic that says that if a task is marked as “Complete” then it’s progress must be 100%. To support this behaviour we’ll amend the TaskBase’s Progress property:

    public float Progress {
        get {
            if(this.Status == this.StatusSet.LastStatus)
                return 1;
    
            return TotalHours > 0 ? HoursSpent / TotalHours : 0;
        }
    }

    And there we go, we’re all done for this post. If you want to follow along at home then you can download the code from here. In the next post we’ll refactor our status system so that a user can choose a status from a list of possible values. In the meantime, happy XAFing!

More Posts Next page »
Copyright © 1998-2010 Developer Express Inc.
ALL RIGHTS RESERVED