Jan 16 2012

SwimEventTimes W7P Development -- Live Tile

Category: � Administrator @ 12:18

If you have an app then you owe it to yourself and users of your app to get something relevant onto the "pinned" Application Tile.  For a user who has "pinned" your application they will quickly expect something to appear on the back portion of the application tile.  This is what people consider the "cool" factor of owning a Windows Phone.  Having my phone open in the elevator with lots of flipping tiles makes the Windows 7 Phone look impressive to those other phone owners.  If your application tile does not flip and reveal any tidbits of information then your app is doomed to live unpinned and probably unused.

What I decided to do for my app was to have the user of the app select which swimmer(s) stat would appear on the Live Tile.  Then when the agent code (usually every 30 minutes) runs it will select a ("Best") random stat from the selected swimmer(s).  This way every 30 minutes I get a new stat for a swimmer on the back of the Live Tile.  You only have about 45 characters to play with so be selective about what you will show on the back of the Live Tile.

 

Now for the code:

From your main page include the start of the agent in the constructor:

// Constructor
        public RequestedSwimmersPage()
        {
            InitializeComponent();
           
            AgentMgr.StartAgent();      
            
        }

I've added a separate class to handle the use of the agent.  Keeps the code cleaner for future changes:

public static class AgentMgr
    {
        private const String AgentName = "SwimmerAgent";
        private const String AgentDescription = "Custom background agent for Swim Event Times pinned Tile!";


        public static void StartAgent()
        {
            StopAgentIfStarted();

            PeriodicTask task = new PeriodicTask(AgentName);
            task.Description = AgentDescription;
            ScheduledActionService.Add(task);
#if DEBUG 
        // If we're debugging, attempt to start the task immediately
            ScheduledActionService.LaunchForTest(AgentName, new TimeSpan(0, 0, 1)); 
#endif
        }


        public  static void StopAgentIfStarted()
        {
            if (ScheduledActionService.Find(AgentName) != null)
            {
                ScheduledActionService.Remove(AgentName);
            }
        } 
         
    }
}

 

So to get things moving you'll need a separate project (for the Agent worker) added to your solution to have the agent run.  Select the "Scheduled Task Agent" project type when adding the project to your solution.  Once you have a separate (Agent) project then add a reference to that from your main application.

It runs on a 30 minute interval so the data that I'm pulling will change on the back of the pinned Application Tile on that interval.

Now the Agent project will have one class and one method (OnInvoke) that you need to be concerned with.  Place your code to get new information onto the back tile in this method:

/// <summary>
        /// Agent that runs a scheduled task
        /// </summary>
        /// <param name="task">
        /// The invoked task
        /// </param>
        /// <remarks>
        /// This method is called when a periodic or resource intensive task is invoked
        /// </remarks>
        protected override void OnInvoke(ScheduledTask task)
        {
        
            //Here is where you put the meat of the work to be done by the agent

            //get the data from ISO 
            ObservableCollection<Swimmer> Swimmers = BackgroundAgentRESTCall.GetSwimmers();
            ObservableCollection<RequestedSwimmer> RequestedSwimmers = BackgroundAgentRESTCall.GetRequestedSwimmers();

            //pull out those swimmers who have the Live Tile turned on
            //and only those that match the 'Course' selection
            IEnumerable<Swimmer> tileSwimmers = from swimmer in Swimmers
                                            join reqswimmer in RequestedSwimmers on swimmer.SwimmerID equals reqswimmer.SwimmerID
                                            where reqswimmer.UseOnLiveTile == true
                                            && reqswimmer.BestCourseSelection == swimmer.Course
                                            select swimmer;


            string tileText = String.Empty;
            string titleText = string.Empty;
            
            //make sure that we have at least one stroke for a swimmer.
            if (tileSwimmers.Count() != 0)
            {

                //pull out the best swims for all marked (Live Tile) swimmers
                var bestswims = from p in tileSwimmers
                                //where conditions or joins with other tables to be included here                           
                                group p by p.SwimmerID + p.Stroke + p.Course + p.Distance into grp
                                let MinTime = grp.Min(g => g.TimeSecs)
                                from p in grp
                                where p.TimeSecs == MinTime
                                orderby p.Course descending, p.StrokeOrder, p.Distance
                                select p;

                int count = bestswims.Count(); // 1st round-trip 
                int index = new Random().Next(count);

                var tileSwimmer = bestswims.Skip(index).FirstOrDefault(); // 2nd round-trip 
              

                //foreach (Swimmer tileSwimmer in tileSwim)
                //{
                //Debug.WriteLine("WWW-data.LastProcessToTouchFile=" + tileSwimmer.AltAdjTime);
                titleText = tileSwimmer.Distance + tileSwimmer.Stroke + ": " + tileSwimmer.Time;

                Guid NavGUID;
                NavGUID = (Guid)tileSwimmer.SwimmerID;

                var tileReqSwimmer = (from reqswimmer in RequestedSwimmers
                                      where reqswimmer.SwimmerID == NavGUID
                                      select reqswimmer).FirstOrDefault();

                int LastnamLen = tileReqSwimmer.LastName.Length;
                if (LastnamLen > 9)
                {
                    LastnamLen = 9;
                }


                int StandardLen = tileSwimmer.Standard.Length;
                if (StandardLen > 6)
                {
                    StandardLen = 6;
                }

                tileText = tileReqSwimmer.FirstName.Substring(0, 1) + tileReqSwimmer.LastName.Substring(0, LastnamLen)
                + " Age: " + tileSwimmer.Age + "   "
                + String.Format("{0:MMM yyyy}", tileSwimmer.MeetDate) + "  "
                + tileSwimmer.Course + "  " + tileSwimmer.Standard.Substring(0, StandardLen);
            }
            else
            {
                tileText = "Select a Swimmer to be shown.";
                titleText = "Event & Time";
            }

            UpdateAppTile(tileText, titleText);

           
        }

The above routine gets the data from Isolated Storage. 

Note: I used a mutex in the call for the ISO data since we could be writing to the same storage at the time we are trying to read from it.

  I made heavy use of the sample mutex code listed from the link below:
 http://www.31a2ba2a-b718-11dc-8314-0800200c9a66.com/2011/11/this-is-continuation-of-this-post.html

Then I used LINQ to pull out the data needed, format it and call the Update method for the Tile:

private void UpdateAppTile(string message, string backTitleText)
        {
            ShellTile appTile = ShellTile.ActiveTiles.First();
            if (appTile != null)
            {
                StandardTileData tileData = new StandardTileData
                {
                    BackContent = message
                    ,BackBackgroundImage = new Uri("/Images/LiveTileC173x173.png", UriKind.Relative)
                    ,BackTitle = backTitleText
                };

                appTile.Update(tileData);
            }
        }

That gives us a pinned Application Tile with swimmer(s) changing info. 

Swimmers like it since it randomly rotates through their "Best" swim times and it gives the app that cool flipping tile that other phone owners love to hate.

 

 

 

 

Tags: , , , , ,

Dec 11 2011

Swim Event Times W7P Development -- BeginGetRequest

Category: � Administrator @ 07:22

Now the engine that's the heart of this application lies in its ability to scrape a URL.  To do that we need to use the asynchronous calls (BeginGetRequest) out to a URL in the hope that we eventually get a page's worth of data back.  We also need to control the call by means of a timeout. 

So let's talk about the functional setup and what the code has to handle.

To complicate things, the website that I'm hitting is an ASP.Net application which does not use cookies stored on the client.  They're using session state to maintain position within the list of pages that you have requested. The site also does not use the QueryString in any form on the URL, which also makes it a bit stickier since you cannot hit a desired page straight away. You always have to start with the Search page and pump through the rest of the pages.  All of these little things added up to an annoying set of problems. 

From purely a user experience, the site design is poor since they make you re-enter (no cookies) the same information every time you visit the site.   Maybe this is by design but it really does not lend itself to a good customer experience. There's also no mobile support which means that using your phone to hit the site is a real boondoggle.

Now for the code.

 

To kick off any request to a URL you'll need to do it on a separate thread:

public void SendPost()
        {           
            // Create a background thread to run the web request
            Thread t = new Thread(new ThreadStart(SendPostThreadFunc));
            t.Name = "URLRequest_For_" + "TODO";
            t.IsBackground = true;
            t.Start();
        }

 

Next we need to keep the primed Request Stream since we need it on subsequent calls to the site.  So in this case we use BeginGetRequestStream:

void SendPostThreadFunc()
        {

            //test the network first
            if (online == false)
            {
                this.Dispatcher.BeginInvoke(() =>
                {
                    progressBar1.IsLoading = false;
                    MessageBox.Show("Network Disconnected.  Please try again when you have a good Network signal.");
                });
                return;
            }


            // Create the web request object
            try
            {
                HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(CookieColUri);

                //Trying to use the THreadPool for timeout  -- waiting on code
                ThreadPool.QueueUserWorkItem(state =>
                                               {

                                                   webRequest.Method = "POST";
                                                   webRequest.ContentType = "application/x-www-form-urlencoded";
                                                   webRequest.CookieContainer = cookieJar;

                                                   // RequestState is a custom class to pass info
                                                   RequestState reqstate = new RequestState();
                                                   reqstate.Request = webRequest;
                                                   reqstate.Data = "passed data";

                                                   webRequest.BeginGetRequestStream(GetReqeustStreamCallback, reqstate);

                                               }
                                               );


            }


            catch (Exception ex)
            {
                //Debug.WriteLine(" --> BGRS3 Exception: " + ex.Message + ", Thread: " + Thread.CurrentThread.ManagedThreadId); 

                // notify your app of a problem here
                this.Dispatcher.BeginInvoke(() =>
                {
                    progressBar1.IsLoading = false;
                    MessageBox.Show("BGRS3: " + ex.Message);
                });

            }


        }

 Now we handle the callback from the BeginGetRequestStream (lots of error handling):

 

void GetReqeustStreamCallback(IAsyncResult asynchronousResult)
        {

            if (!asynchronousResult.IsCompleted)
                return;

            RequestState reqstate = null;

            try
            {
                // grab the custom state object
                reqstate = (RequestState)asynchronousResult.AsyncState;

                //Thread.Sleep(15000);  // uncomment this line to test the timeout condition

                HttpWebRequest webRequest = (HttpWebRequest)reqstate.Request;                

                // End the stream request operation
                Stream postStream = webRequest.EndGetRequestStream(asynchronousResult);

                // Create the post data
                string postData = "";
                for (int i = 0; i < paramNames.Count; i++)
                {

                    if (paramNames[i] == "POSTDATA")
                    {
                        postData = paramValues[i];
                        break;

                    }
                    else
                    {
                        // Parameter seperator
                        if (i > 0)
                        {
                            postData += "&";
                        }

                        // Parameter data
                        postData += paramNames[i] + "=" + paramValues[i];
                    }
                }
                byte[] byteArray = Encoding.UTF8.GetBytes(postData);

                // Add the post data to the web request
                postStream.Write(byteArray, 0, postData.Length);
                postStream.Close();


                ThreadPool.QueueUserWorkItem(new WaitCallback(target =>
              {
                  try
                  { // you must have this try-catch here to handle exceptions from the callback                      

                      // RequestState is a custom class to pass info
                      RequestState reqstate2 = new RequestState();
                      reqstate2.Request = webRequest;
                      reqstate2.Data = "passed data";
                      reqstate2.AllDone = new AutoResetEvent(false);
                      
                      IAsyncResult result = (IAsyncResult)webRequest.BeginGetResponse(new AsyncCallback(GetResponseCallback), reqstate2);

                      bool waitOneResult = true;

                      if (!reqstate2.AllDone.WaitOne(DefaultTimeout))
                      {
                          waitOneResult = false;

                          if (webRequest != null)

                              webRequest.Abort();
                      }
                      
                  }
                  catch (WebException webExcp)
                  {                   

                      WebExceptionStatus status = webExcp.Status;
                      if (status == WebExceptionStatus.ProtocolError)
                      {
                          // Get HttpWebResponse so that you can check the HTTP status code.
                          HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response;                   

                          this.Dispatcher.BeginInvoke(() =>
                              {
                                  progressBar1.IsLoading = false;
                                  MessageBox.Show("Unable to reach site. Please try later! " + (int)httpResponse.StatusCode + " - "
                                 + httpResponse.StatusCode + ".");
                              });
                      }
                  }

                  catch (Exception ex)
                  { // you must handle the exception or it will be unhandled and crash your app
                      
                      // notify your app of a problem here
                      this.Dispatcher.BeginInvoke(() =>
                      {
                          progressBar1.IsLoading = false;
                          MessageBox.Show("BGR1: " + ex.Message);
                      });
                  }


              }
                                  ));
            }


            catch (WebException webExcp)
            {               
                WebExceptionStatus status = webExcp.Status;
                if (status == WebExceptionStatus.ProtocolError)
                {
                    // Get HttpWebResponse so that you can check the HTTP status code.
                    HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response;                    

                    this.Dispatcher.BeginInvoke(() =>
                        {
                            progressBar1.IsLoading = false;
                            MessageBox.Show("Unable to reach site." + (int)httpResponse.StatusCode + " - "
                           + httpResponse.StatusCode + ".");
                        });
                }

            }
            catch (Exception ex)
            {
                // notify your app of a problem here
                this.Dispatcher.BeginInvoke(() =>
                    {
                        progressBar1.IsLoading = false;
                        MessageBox.Show("BGR3: " + ex.Message);
                    });             
            }            

        }

 Now get the response from the website:

void GetResponseCallback(IAsyncResult asynchronousResult)
        {

            if (!asynchronousResult.IsCompleted)
                return;

            // grab the custom state object
            RequestState reqstate = (RequestState)asynchronousResult.AsyncState;

            //Thread.Sleep(50000);  // uncomment this line to test the timeout condition 50 seconds (timeout 45)

            try
            {
                HttpWebRequest webRequest = (HttpWebRequest)reqstate.Request;                

                // End the get response operation
                HttpWebResponse response = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);               

                Stream streamResponse = response.GetResponseStream();
                StreamReader streamReader = new StreamReader(streamResponse);
                Response = streamReader.ReadToEnd();
                streamResponse.Close();
                streamReader.Close();
                response.Close();

                // Call the response callback
                if (callback != null)
                {
                    callback();
                }

            }
            catch (WebException webExcp)
            {
                // If you reach this point, an exception has been caught.           
                WebExceptionStatus status = webExcp.Status;
                if (status == WebExceptionStatus.ProtocolError)
                {
                    // Get HttpWebResponse so that you can check the HTTP status code.
                    HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response;                 

                    this.Dispatcher.BeginInvoke(() =>
                        {
                            progressBar1.IsLoading = false;
                            MessageBox.Show("Unable to reach site." + (int)httpResponse.StatusCode + " - "
                           + httpResponse.StatusCode + ". Launching browser directly at site to show error!");

                        });

                    //Launcher for main page in which we got the error.
                    WebBrowserTask webBrowserTask = new WebBrowserTask();
                    webBrowserTask.URL = CookieColUri.ToString();
                    webBrowserTask.Show();

                    return;
                }
                else
                {
                    if (status == WebExceptionStatus.RequestCanceled)
                    { //abort from time -out                        
                        this.Dispatcher.BeginInvoke(() =>
                            {
                                progressBar1.IsLoading = false;
                                MessageBox.Show("Network Connection lost.  Please try when you have a good Network signal.");
                            });
                        return;
                    }
                    else
                    {
                        this.Dispatcher.BeginInvoke(() =>
                        {
                            progressBar1.IsLoading = false;
                            MessageBox.Show("Request lost.  Please try when you have a good Network signal.");
                        });
                        return;

                    }

                }
            }
            catch (Exception excp)
            {
                this.Dispatcher.BeginInvoke(() =>
                {
                    progressBar1.IsLoading = false;
                    MessageBox.Show("Request for Swimmer lost.  Please try when you have a good Network signal.");
                });
                return;
            }


            reqstate.AllDone.Set();

        } 

 

Now go ahead and use the HTML Agility Pack on the return results (in the callback) to strip any data you want from that page.

Note:  You must use the timeout on these calls otherwise you'll have a zillion crashes in your app. 

The timeout code was provided by Dan Colasanti.  www.improvisoft.com/blog (I owe him many beers!)

 

Tags: , , , , ,

Nov 2 2011

SwimEventTimes W7P Development -- Isolated Storage and Tombstoning

Category: � Administrator @ 17:10

At the outset of development I really didn't look into tombstoning and what that entailed but I knew I had to make use of Isolated Storage for keeping the content resident on the phone once it had been "SCRAPED" from the target URL.  You're asking yourself why am I talking about Isolated Storage and tombstoning together.  Well as it turn out, getting your app back to the state that it was in prior to tombstoning requires Isolated Storage.  I have designed my Isolated Storage with a GUID as its key:

Guid SwimmerGuid;
SwimmerGuid = Guid.NewGuid();

Once I have the data from the Target URL I generate a new GUID and that becomes the key that is passed from one page to the next.  In the "navigatedTo" pages, this is passed in the QueryString (think ASP.NET) and the rest of the data values are re-hydrated from Isolated Storage:

this.NavigationService.Navigate(new Uri("/MeetDetails.xaml?selectedGUID=" +
                    curSwimmer.SwimmerID + "&Meet=" + meetName, UriKind.Relative));

This makes easy work of adding new pages since you can construct your called page and know that you'll have the data available from Isolated Storage.

I'm talking about Isolated Storage as if I'm constantly hitting those files but in reality I simply read the stored data (into ObservableCollection) when the App starts up and I rewrite it when I add new data from the target URL:

private void Application_Activated(object sender, ActivatedEventArgs e)
        {
            this.LoadSavedApplicationState();
        }
void LoadSavedApplicationState()
 {
     if (Database.Instance.RequestedSwimmers == null)
     {
         using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
         {
             if (store.FileExists("RequestedSwimmers.xml"))
             {
                 using (IsolatedStorageFileStream stream = store.OpenFile("RequestedSwimmers.xml", System.IO.FileMode.Open))
                 {
                     XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection<RequestedSwimmer>));
                     Database.Instance.RequestedSwimmers = (ObservableCollection<RequestedSwimmer>)serializer.Deserialize(stream);
                 }
             }
             else
             {
                 Database.Instance.RequestedSwimmers = new ObservableCollection<RequestedSwimmer>();
             }

        }

    }

When I need to write, I add new swimmers to the ObservableCollection and write it back to Isolated Storage:

if (Database.Instance.RequestedSwimmers != null)
         {
             using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
             {
                 using (IsolatedStorageFileStream stream = store.CreateFile("RequestedSwimmers.xml"))
                 {
                     XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection<RequestedSwimmer>));
                     serializer.Serialize(stream, Database.Instance.RequestedSwimmers);

                 }
             }
         }

 

This way Isolated Storage is always up to date.

Resuming from tombstoning is straight forward since all the pages are called with a QuerySring (GUID) and the ObservableCollection has already been re-hydrated from Isolated Storage.

 

 

 

Tags: , , , , ,

Oct 2 2011

SwimEventTimes W7P Development -- HTML Agility Pack

Category: � Administrator @ 08:13

When this app started out, a long time ago, I happened upon the HTML Agility Pack or HAP.  This tool was originally written for .Net and used XPath to search and define the elements that you needed from the original source HTML.  So when it comes time to SCRAPE data from a page, you'll need a tool that provides a robust and almost carefree nature about the HTML structure.  It does so much for you that without it I would have never attempted what I was thinking:

http://htmlagilitypack.codeplex.com/

Kudos to the authors, especially DarthObiwan, of this fantastic piece of work.

Basically it allows you to define what tags you want from a page and search and pull out that piece of the text.  But the twist here is that for HAP to work on the phone it had to work without Xpath since Xpath was not supported in the OS 7.0 release on the phone.

So what took the place of the Xpath is LINQ.  This added yet another dimension to my learning since I had never really used LINQ and had only recently started looking into using LINQ when I thought about converting a project from XSLT.  It takes time to make the mental switch from Xpath to LINQ but there was no other way.  Also, at that time, none of the code releases for HAP worked on the phone but by getting the source and following the comments of the authors on how to re-engineer the code I was able to get it compile and now it works like a charm.

So now let's talk about how we use HAP:

Here we have entire HTML loaded up in htmDoc.

//let's detemine what came back on the response

HtmlAgilityPack.HtmlDocument htmDoc = new HtmlAgilityPack.HtmlDocument();
htmDoc.LoadHtml(responseData);

Next you can start to look for specific items:

string pattern = @".*txtSearchLastName$"; 
var SearchPagenode = htmDoc.DocumentNode
                      .Descendants("input")
                      .FirstOrDefault(x => Regex.IsMatch(x.Id, pattern));


So now I can look at the element and get the id:

CTLID = SearchPagenode.Id;

Other things like pulling out <a> tags out of table contained in the last <tr>:

pattern = @".*dgPersonSearchResults$"; 
var links = htmDoc.DocumentNode.Descendants("table")
           .First(n => Regex.IsMatch(n.Id, pattern))
          .Elements("tr").Last() .Descendants("a")
          .Select(x => x.GetAttributeValue("href", "")).ToArray();


You can go crazy with HAP and as your LINQ gets better you can go further and refine these queries.

HAP provides the foundation and performs the grunt work required to interrogate/parse a HTML source.


Tags: , , , ,

Aug 6 2011

W7P SwimEventTimes development - Touch and Hold Gesture vs Discovery

Category: Features � Administrator @ 11:20

I had implemented a "Touch and Hold" gesture for editing and deleting items from the Swimmer's page. 

The "Touch and Hold" would bring up a Context menu for editing and deleting a selected swimmer.

After receiving complaints from beta users that this was not discoverable I thought I needed to provide a better experience for the user. 

What I ended up using was a List box with check boxes and additional icons in the application bar to control the experience.  So before, I had a context menu with "Edit" and "Delete" which was revised into two additional icons on the application bar.

I made great use of the following example:

http://listboxwthcheckboxes.codeplex.com/

Now it's quite apparent on how to delete swimmers and edit the swimmer's date range when you need to refresh and download with the latest meet data.

 

 

 

Tags: , , , ,