Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF

GoalBook - A Hybrid Smart Client

Rate me:
Please Sign up or sign in to vote.
4.86/5 (24 votes)
25 Sep 2009CPOL10 min read 78.6K   834   69   16
A WPF hybrid smart client that synchronises your goals with the Toodledo online To-do service.

Introduction

GoalBook is a hybrid smart client desktop manager application for the Toodledo online to-do service. GoalBook runs locally on your PC utilising the threading, storage, and printing capabilities of Windows, and synchronizes the Toodledo web hosted data when connected to the internet, yet still provides rich functionality when disconnected from the internet. GoalBook has been built with the Windows Presentation Foundation (WPF), Visual Studio 2008, and .NET 3.5 (SP1), and demonstrates a number of technologies including Composite Application Library (CAL), LINQ, REST, CSLA.NET, and Infragistics XamDatagrid. If you haven't used the CAL, you might want to look at Getting Started with the Composite Application Library. To use GoalBook to access Toodledo online, you'll need to get a (free) Toodledo account.

GoalBook.png

Retrieve data from Toodledo using REST and LINQ

The Toodledo API is exposed using a simple REST interface. REST avoids the complexity of other network mechanisms, and is usually implemented by simple HTTP requests. An important concept to understand with REST is that you need to know the format of the REST response (it might be XML, JSON, or something else). This differs from other Web Service protocols, e.g., SOAP, where the response is defined by WSDL. REST requires reading of the documentation and/or some trial and error.

The following example request gets the login token from Toodledo:

http://api.toodledo.com/api.php?method=getToken;userid=td123f4567b8910

The result of this HTTP request is an XML fragment:

XML
<token>td12345678901234</token>

All requests to the Toodledo API follow this simple pattern, with the additional requirement of an authentication key where user data is requested. The authentication key is generated using an MD5 hash of the token, user ID, and the password. (Note: the password is first hashed and then hashed again with the token and the user ID.)

C#
/// <summary>
/// Get SessionKey.
/// </summary>
protected string GetSessionKey()
{
  ...

  //Create SessionKey.
  return MD5(MD5(ConnectInfo.Password) + _token + ConnectInfo.UserID);
}

The request for goals uses the following HTTP request:

http://api.toodledo.com/api.php?method=getGoals;key=317a1aae7a6cc76e2510c8ade6e6ed05

The result of this HTTP request is an XML fragment:

XML
<goals>
  <goal id="123" level="0" contributes="0" archived="1">Get a Raise</goal>
  <goal id="456" level="0" contributes="0" archived="0">Lose Weight</goal>
  <goal id="789" level="1" contributes="456" archived="0">Exercise</goal>
</goals>

GoalBook makes these HTTP requests in C# using the HttpWebRequest and HttpWebResponse classes. When data is requested from Toodledo, an HTTP GET is used. Parameters required for the request are encoded using HttpUtility.UrlEncode and sent in the URL. Proxy authentication details can be specified for users on a LAN with the WebProxy class. The results received from Toodledo are loaded into an XDocument.

C#
/// <summary>
/// Make Server Call using Http Get.
/// </summary>
/// <param name="url">url (parameters separated by ;)</param>
/// <returns>XDocument</returns>
private XDocument HttpGet(string url)
{
    HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

    if (HostInfo.Enabled && HostInfo.IsValid)
    {
        //Set manual HTTP Proxy configuration.
        request.Proxy = new WebProxy(HostInfo.Host, Convert.ToInt32(HostInfo.Port));
        request.Proxy.Credentials = CredentialCache.DefaultCredentials;
    }

    XDocument result = null;
    using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
    using (StreamReader reader = new StreamReader(response.GetResponseStream()))
    using (XmlTextReader textReader = new XmlTextReader(reader))
    {
        // Load the response into an XDocument.
        result = XDocument.Load(textReader);
    }

    return result;
}

When data is uploaded to Toodledo, e.g., when adding or editing records, HTTP POST is used. Parameters are also encoded using HttpUtility.UrlEncode and sent as form data (note that parameters for posted data are separated by the ampersand):

url: http://api.toodledo.com/api.php
parameters: method=addGoal&key=317a1aae7a6cc76e2510c8ade6e6ed05&
                    title=Get+another+Raise&level=1

The response received from Toodledo is an XML fragment indicating the result of the post, and is loaded into an XDocument:

C#
/// <summary>
/// Make Server Call using Http Post.
/// </summary>
/// <param name="url">url parameter</param>
/// <param name="parameters">parameters (separated by &)</param>
/// <returns>XDocument</returns>
private XDocument HttpPost(string url, string parameters)
{            
    byte[] data = UTF8Encoding.UTF8.GetBytes(parameters);

    HttpWebRequest request = WebRequest.Create(new Uri(url)) as HttpWebRequest;
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.ContentLength = data.Length;

    if (HostInfo.Enabled && HostInfo.IsValid)
    {
        //Set manual HTTP Proxy configuration.
        request.Proxy = new WebProxy(HostInfo.Host, Convert.ToInt32(HostInfo.Port));
        request.Proxy.Credentials = CredentialCache.DefaultCredentials;
    }
    
    using (Stream stream = request.GetRequestStream())
    {
        // Write the request.
        stream.Write(data, 0, data.Length);
    }
    
    XDocument result = null;
    using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
    using (StreamReader reader = new StreamReader(response.GetResponseStream()))
    using (XmlTextReader textReader = new XmlTextReader(reader))
    {
        // Load the response into an XDocument.
        result = XDocument.Load(textReader);
    }

    return result;
}

XDocument is a key component of the .NET 3.5 LINQ to XML technology. LINQ queries can be executed against an XDocument (note in the example below that Archived goals are filtered out, and the result is ordered by Level). The result of the LINQ query is a System.Linq.OrderedEnumerable<XElement> that can be iterated over using foreach:

C#
//Filter and order the result list.
var orderedServerList = from s in serverList.Descendants(GOAL)
                        where int.Parse(s.Attribute(GOAL_ARCHIVED).Value) == 0
                        orderby s.Attribute(GOAL_LEVEL).Value
                        select s;

//Iterate the result.
foreach (XElement element in orderedServerList){ ... }

Synchronizing in a background thread

The synchronization of data with Toodledo occurs in a background thread using the BackgroundWorker class. Progress is reported back to the application and displayed in the status bar. Note that cloned lists are passed to the background worker thread because the lists are owned by the UI thread (they are bound to the XamDatagrid). It is a basic rule of .NET programming that a thread is not allowed to access an object owned by another thread and an exception will occur if you attempt to do this. The BackgroundWorker instance handles transitioning of objects back to the calling thread so it is safe to work directly with BackgroundWorker responses. The response lists are merged into their original list to complete the synchronization when the synchronizer callback method is invoked.

C#
/// <summary>
/// Handle Synchronisor_SyncCompleted event.
/// </summary>
private void Synchronisor_SyncCompleted(SyncCompletedEventArgs e)
{
    //Merge changes.
    lock (_persistenceService.Goals) { _persistenceService.Goals.Merge(e.Data.Goals); }
    lock (_persistenceService.Folders) { _persistenceService.Folders.Merge(e.Data.Folders); }
    lock (_persistenceService.Notes) { _persistenceService.Notes.Merge(e.Data.Notes); }
    lock (_persistenceService.Tasks) { _persistenceService.Tasks.Merge(e.Data.Tasks); }
    ...
}

Saving XDocument to Isolated Storage

GoalBook data is saved as XML into Isolated Storage using the XDocument class functionality. XDocument automatically handles special characters in text strings, e.g., ampersands (&) etc., by encoding/decoding as required. Note that GoalBook can work offline for extended periods without synchronizing. Saved data is retrieved from Isolated Storage when GoalBook starts up. A StreamWriter is used to save the output from the GetGoalsXDocument() method.

C#
/// <summary>
/// Save To IsolatedStorage.
/// (e.g. C:\Users\[UserName]\AppData\Local\IsolatedStorage).
/// </summary>
/// <param name="document">XDocument to save</param>
/// <param name="file">Name of file</param>
private void SaveToIsolatedStorage(XDocument document, string file)
{
    // Open the stream from IsolatedStorage.
    IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
        file, FileMode.Create, GetUserStore());
    using (stream)
    {
        using (StreamWriter writer = new StreamWriter(stream))
        {
            document.Save(writer);
        }
    }
}

Manipulate data with a business objects framework

CSLA.NET is the business objects framework used by GoalBook. The role of a business objects framework is to provide support for data binding, rules validation, state management, and more. Business objects provide the mechanisms used for binding application data to the user interface. A useful feature provided by CSLA.NET is multi-level undo. You can access this functionality from GoalBook's edit menu (or Ctrl-Z). All add, delete, and edit actions can be undone. Filtering of data in the GoalBook user interface is enabled using the CSLA FilteredBindingList class. FilteredBindingList requires its FilterProvider property to be set to a method with a boolean return value. Each item in the source list is evaluated by this method to determine if it should appear in the filtered list. Since the filtered list is just a wrapper around the source list, any edits made to databound list items occur directly against the source list. FilteredBindingList is initialised from a standard CSLA BusinessListBase. For example:

C#
this.Model.Goals = new FilteredBindingList<goal />(_persistenceService.Goals);
this.Model.Goals.FilterProvider = this.LevelFilter;
this.Model.Goals.ApplyFilter(Goal.LevelProperty.Name, this.ActionView.Filter);

CSLA provides the capability, through a built-in rules engine, to validate each object (Goal, Note, Credentials etc.) using either rules pre-defined by CSLA (e.g., StringRequired), or through the definition of custom methods that are executed when the object is validated. The validation mechanism is exposed through the standard IDataErrorInfo interface, and is used by GoalBook dialogs to indicate validation errors (mouse over the error image to see the validation message). For example:

C#
/// <summary>
/// EmailValidator.
/// Called by BusinessBase RulesEngine.
/// </summary>
public static bool EmailValidator<T>(T target, RuleArgs e) where T : Credentials
{
    //Validate Email.
    const string emailRegex = @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*
                @([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$";

    if (Regex.IsMatch(target.Email, emailRegex)) { return true; }
    else { e.Description = Resources.ValidationEmailMessage; }

    return false;
}

Account

Modularity with the Composite Application Library

Modularity is built into GoalBook with the Composite Application Library (CAL) from The Patterns & Practices team at Microsoft. The loading of modules can be dynamic (e.g., a folder path can be specified) or static (individual modules are specified). GoalBook uses static loading because the modules are known. This is a key design decision where the GoalBook Shell has significant knowledge about the purpose of the application and only hands off editing and display capability to the modules. Defining how the modules will load occurs in the Bootstrapper class. For example:

C#
/// <summary>
/// Get Module Catalog. Statically loaded modules.
/// </summary>        
protected override IModuleCatalog GetModuleCatalog()
{
    return new ModuleCatalog()
        .AddModule(typeof(TasksModule))
        .AddModule(typeof(NotesModule))
        .AddModule(typeof(GoalsModule));                   
}

The Bootstrapper class inherits from the UnityBootstrapper. UnityBootstrapper is an Inversion of Control (IOC) Container, and is responsible for the dynamic initialisation of classes. The GoalBook Shell and modules use direct references to the IUnityContainer interface. This is the approach recommended in the CAL documentation: "Containers have different semantics of usage, which often drives the decision for why that container is chosen". The Unity Container uses Reflection to inspect the class being initialised, and injects the required parameters. This pattern of initialisation is called Dependency Injection, or more specifically, Constructor Injection. This provides a more decoupled architecture than the traditional interface approach to modular applications. You use the Unity Container's Resolve method to dynamically initalise class instances. For example:

C#
Model = _unityContainer.Resolve<MainPresentationModel>();

CAL views are 'active aware' when the IActiveAware interface is implemented. This enables global commands to be routed to a registered event handler in the currently active view only. E.g., when the Delete command executes, only the currently active view gets a chance to handle the event. Views implementing the IActiveAware interface are notified each time the active view changes, and must raise this event to the associated presenter class so the active state of local command references can be set.

C#
/// <summary>
/// Handle View_IsActiveChanged event.
/// </summary>
/// <param name="sender">Source of caller</param>
/// <param name="e">EventArgs parameter</param>
private void View_IsActiveChanged(object sender, EventArgs e)
{            
    this.printCommand.IsActive =
    this.undoCommand.IsActive =
    this.deleteCommand.IsActive = this.View.IsActive;
}

Presentation with Infragistics XamDatagrid

The Infragistics XamDatagrid (Express Edition) is the datagrid control used for presentation in GoalBook. The datagrid is a ubiquitous control that presents data in a tabular style that can easily be sorted and scrolled. The XamDatagrid binds directly to lists in the module's DataContext (in this case, the Goals collection). Binding is one way only - editing occurs in dialogs, and changes are automatically refreshed though property changed events raised by the business object that has been edited. For example:

XML
<igDP:XamDataGrid DataSource="{Binding Path=Goals, Mode=OneWay}" ... >

Some fields displayed as strings in the XamDatagrid are represented as integers (foreign keys) in the business object. This conversion occurs during databinding by specifying the converter to use in XAML. The example below uses a custom IntegerForiegnKeyConverter to display the Goal Level. Note that the Label text is referenced from a resource file. This allows for localisation of user viewable strings (currently, only an English resource file exists). For example:

XML
<igDP:Field Name="LevelID" Label="{x:Static ModuleProperties:Resources.LabelLevel}" 
    Converter="{StaticResource integerForeignKeyConverter}" 
    ConverterParameter="{StaticResource levelReferenceData}">

Authoring notes with the WPF RichTextBox

GoalBook provides a sophisticated WSIWYG notes editor that utilises the WPF RichTextBox. Notes authored in GoalBook are synchronised with Toodledo's notebook. The GoalBook notes editor takes a Flow Document as its datasource and saves this locally as XAML. Notes are converted to text with HTML formatting when uploaded to Toodledo.

Note.png

A number of technical challenges were encountered while building GoalBook's notes editor. The brief was to produce a note compliant with Toodledo's notes formatting (basic text with HTML tags, e.g., bold, italic, hyperlink, numbered lists, and bulleted lists). The conversion between the Toodledo note format and the FlowDocument rendered in the WPF RichTextBox occurs in several steps, and utilises two HTML parsing libraries. The first library converts XAML to HTML (with styles), while the second library provides a character by character parsing where HTML styles are replaced with the basic HTML tags supported by Toodledo (<b>, <i>, <a>, <ul>, <ol>, and <li>). The source code for both of these libraries is included in the GoalBook.Public project. The final challenge for the GoalBook notes editor was to allow for the activation of hyperlinks. First, enable the FlowDocument by setting the RichTextBox's IsDocumentEnabled property to true. Each hyperlink can then be hooked to an an event handler. For example:

C#
/// <summary>
/// Locate And Hook Hyperlinks.
/// </summary>
/// <param name="flowDocument">FlowDocument parameter</param>
private void LocateAndHookHyperlinks(FlowDocument flowDocument)
{            
    FlowDocumentHelper helper = new FlowDocumentHelper(flowDocument);
    foreach (Hyperlink hyperlink in helper.GetHyperlinks())
    {
        this.HookHyperlink(hyperlink);
    }
}

/// <summary>
/// Hook Hyperlink to event handler.
/// </summary>
/// <param name="hyperLink">Hyperlink parameter</param>
private void HookHyperlink(Hyperlink hyperLink)
{
    hyperLink.Click += this.Hyperlink_Click;
    hyperLink.Focusable = false;
    hyperLink.ToolTip = string.Format(
        "Ctrl + click to follow link: {0}", 
        hyperLink.NavigateUri);
}

/// <summary>
/// Handle Hyperlink_Click event.
/// </summary>
/// <param name="sender">sender parameter</param>
/// <param name="e">RoutedEventArgs parameter</param>
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
    if (sender is Hyperlink)
    {
        Process p = new Process();
        p.StartInfo.FileName = (sender as Hyperlink).NavigateUri.ToString();
        p.StartInfo.UseShellExecute = true;
        p.StartInfo.RedirectStandardOutput = false;
        p.StartInfo.Arguments = string.Empty;

        p.Start();
    }
}

To locate the hyperlinks, it is necessary to recursively iterate through the Blocks in the FlowDocument (thanks to Mole for revealing the internal structure of the FlowDocument). This occurs in the FlowDocumentHelper class:

C#
/// <summary>
/// Get Hyperlinks.
/// </summary> 
/// <returns>Hyperlink enumerable list</returns>
public IEnumerable<Hyperlink> GetHyperlinks()
{
    return this.LocateHyperlinks(this.flowDocument.Blocks);
}

/// <summary>
/// Locate Hyperlinks.
/// </summary>
/// <param name="blockCollection">BlockCollection parameter</param>
/// <returns>Hyperlink enumerable list</returns>
private IEnumerable<Hyperlink> LocateHyperlinks(BlockCollection blockCollection)
{
    foreach (Block block in blockCollection)
    {
        if (block is Paragraph)
        {
            foreach (Inline inline in (block as Paragraph).Inlines)
            {
                if (inline is Hyperlink)
                {
                    yield return inline as Hyperlink;
                }
            }
        }
        else if (block is List)
        {
            foreach (ListItem listItem in (block as List).ListItems)
            {
                // Recurse Blocks in each ListItem.
                foreach (Hyperlink hyperlink in this.LocateHyperlinks(listItem.Blocks))
                {
                    yield return hyperlink;
                }
            }
        }
    }
}

Printing with the XPS Document Writer

GoalBook's print service uses the XPSDocumentWriter and FlowDocument functionality to print goals, notes, and tasks. Printing occurs asynchronously with an event raised to the calling module when printing has completed. The custom class FlowDocumentPaginator is responsible for preparing each page for printing. This includes inserting a header and a footer.

C#
/// <summary>
/// Print the specified FlowDocument.
/// </summary>
/// <param name="title">title parameter</param>
/// <param name="document">FlowDocument to print</param>
public void Print(string title, FlowDocument document)
{            
    // Return if user cancels.
    if (this.printDialog.ShowDialog().Equals(false)) 
    { 
        return; 
    }
                                      
    // Print the document.
    if (this.printDialog.PrintableAreaWidth > 0 && 
        this.printDialog.PrintableAreaHeight > 0)
    {
        ...
                        
        // Initialise the FlowDocumentPaginator.
        FlowDocumentPaginator paginator = new FlowDocumentPaginator(
            pageDefinition, document);

        // Print asynchronously.
        xpsDocumentWriter.WriteAsync(paginator);
    }            
}

/// <summary>
/// Print Completed.
/// </summary>
/// <param name="sender">sender parameter</param>
/// <param name="e">WritingCompletedEventArgs parameter</param>
private void PrintCompleted(object sender, WritingCompletedEventArgs e)
{
    if (e.Cancelled || e.Error != null)
    {
        this.OnPrintException(new PrintDialogException(
            string.Format(ShellResources.PrintExceptionMessage, e.Error.Message), 
            e.Error));
    }
    else
    {
        this.OnPrintEnd(ShellResources.PrintEndMessage);
    }
}

Roadmap for GoalBook

This GoalBook application represents my explorations into using WPF as a platform for building a scalable, occasionally connected hybrid smart client application. The choices of technology and architecture are mostly complete, and my objectives are now focused on adding features and exploring what it takes to build a quality product. My intention is to support all of the functionality offered by the Toodledo public API for free accounts. I'm looking forward to continuing my explorations along this technology track with an emphasis on an enhanced user experience for Windows 7.

References

History

  • 25th March, 2009: Version 0.3 (Initial version).
  • 21st April, 2009: Version 0.4 (Added printing and proxy server authentication).
  • 25th June, 2009: Version 0.5 (Added Notes module, enhanced printing).
  • 28th July, 2009: Version 0.6 (Added Change account, enhanced dialog service).
  • 24th August, 2009: Version 0.7 (Added Tasks module, enhanced synchronizer).
  • 26th September, 2009: Version 0.8 (Added Action Pane with filtering and search).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Australia Australia
I've been working as a software developer since 2000 and hold a Bachelor of Business degree from The Open Polytechnic of New Zealand. Computers are for people and I aim to build applications for people that they would want to use.

Comments and Discussions

 
QuestionUsername and password sent in plain text? Pin
.dan.g.20-Jan-12 22:06
professional.dan.g.20-Jan-12 22:06 
AnswerRe: Username and password sent in plain text? Pin
Mark Brownsword21-Jan-12 9:33
Mark Brownsword21-Jan-12 9:33 
GeneralRe: Username and password sent in plain text? Pin
.dan.g.22-Jan-12 17:36
professional.dan.g.22-Jan-12 17:36 
Generalbetter than Toodledo's interface Pin
KenJohnson21-Jan-11 9:13
KenJohnson21-Jan-11 9:13 
GeneralRe: better than Toodledo's interface Pin
Mark Brownsword22-Jan-11 9:14
Mark Brownsword22-Jan-11 9:14 
GeneralCannot View Task Notes Pin
aaroncampf14-Jan-11 21:25
aaroncampf14-Jan-11 21:25 
GeneralRe: Cannot View Task Notes Pin
Mark Brownsword17-Jan-11 8:26
Mark Brownsword17-Jan-11 8:26 
GeneralRe: Cannot View Task Notes Pin
aaroncampf17-Jan-11 8:31
aaroncampf17-Jan-11 8:31 
GeneralNicely written, thanks - the recursive GetHyperlinks() method saved me from forced thinking on a Monday (although, you might want to add support for Section element) Pin
kirkaiya16-Aug-09 23:51
kirkaiya16-Aug-09 23:51 
GeneralRe: Nicely written, thanks - the recursive GetHyperlinks() method saved me from forced thinking on a Monday (although, you might want to add support for Section element) Pin
Mark Brownsword17-Aug-09 12:45
Mark Brownsword17-Aug-09 12:45 
GeneralNice Pin
Chorlton Dragon25-Jun-09 1:34
Chorlton Dragon25-Jun-09 1:34 
GeneralRe: Nice Pin
Mark Brownsword25-Jun-09 12:31
Mark Brownsword25-Jun-09 12:31 
GeneralGoalBook wins April draw! Pin
Mark Brownsword19-May-09 9:42
Mark Brownsword19-May-09 9:42 
GeneralRe: GoalBook wins April draw! Pin
Colin Priest19-May-09 11:27
Colin Priest19-May-09 11:27 
GeneralLike it Pin
FantasticFiasco30-Mar-09 23:39
FantasticFiasco30-Mar-09 23:39 
GeneralUse Azure instead of Isolated Storage Pin
Mark Brownsword27-Mar-09 9:06
Mark Brownsword27-Mar-09 9:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.