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.
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:
<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.)
protected string GetSessionKey()
{
...
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:
<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
.
private XDocument HttpGet(string url)
{
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
if (HostInfo.Enabled && HostInfo.IsValid)
{
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))
{
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
:
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)
{
request.Proxy = new WebProxy(HostInfo.Host, Convert.ToInt32(HostInfo.Port));
request.Proxy.Credentials = CredentialCache.DefaultCredentials;
}
using (Stream stream = request.GetRequestStream())
{
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))
{
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
:
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;
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.
private void Synchronisor_SyncCompleted(SyncCompletedEventArgs e)
{
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.
private void SaveToIsolatedStorage(XDocument document, string file)
{
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:
this.Model.Goals = new FilteredBindingList(_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:
public static bool EmailValidator<T>(T target, RuleArgs e) where T : Credentials
{
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;
}
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:
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:
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.
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:
<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:
<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.
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:
private void LocateAndHookHyperlinks(FlowDocument flowDocument)
{
FlowDocumentHelper helper = new FlowDocumentHelper(flowDocument);
foreach (Hyperlink hyperlink in helper.GetHyperlinks())
{
this.HookHyperlink(hyperlink);
}
}
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);
}
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 Block
s in the FlowDocument
(thanks to Mole for revealing the internal structure of the FlowDocument
). This occurs in the FlowDocumentHelper
class:
public IEnumerable<Hyperlink> GetHyperlinks()
{
return this.LocateHyperlinks(this.flowDocument.Blocks);
}
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)
{
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.
public void Print(string title, FlowDocument document)
{
if (this.printDialog.ShowDialog().Equals(false))
{
return;
}
if (this.printDialog.PrintableAreaWidth > 0 &&
this.printDialog.PrintableAreaHeight > 0)
{
...
FlowDocumentPaginator paginator = new FlowDocumentPaginator(
pageDefinition, document);
xpsDocumentWriter.WriteAsync(paginator);
}
}
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).