Correspondence for Silverlight

With Correspondence for Silverlight, you can create occasionally-connected client applications that run either in or out of the browser. They use isolated storage to save the user’s changes before pushing them up to the synchronization service. From there, they are pushed out to other Silverlight or Windows Phone clients. Create a Silverlight application and use NuGet to add a reference to:

Correspondence.Silverlight.AllInOne

The package will add the following features to your application:

  • Main view model
  • View model locator
  • Model
  • Navigation model
  • Synchronization service

Open the Readme.txt file for instructions on adding the view model locator to App.xaml and MainPage.xaml.

To-do list

As an example, try out this to-do list application. Log in using any user name you like; this example app doesn’t authenticate. You can add tasks, update their name, and mark them completed.

Get Microsoft Silverlight

Some things to try. Log in using your name, enter some tasks, then switch to another user. Those tasks disappear. Switch back and they reappear.

Close the browser, then come back to this page. Your data is still here. Disconnect from the network and this still works.

Now go to a different machine and log in using your name again. At first the list will be empty, but you’ll see tasks come down as the app synchronizes.

A web page that lets you share data between two browsers is nothing new. What makes this different is:

  • Data is still accessible while off line. This is particularly important for out-of-browser applications.
  • There is no application-specific server-side schema or code. The data model and all of your logic is in the client.
  • Silverlight applications can synchronize with Windows Phone applications.

Here’s how the to-do list example is built. Download the source code and follow along.

Log on

A User is a fact identified by a user name. We represent the user in the Factual model:

fact User {
key:
    string userName;
}

Any time we create a User with a give user name, we get back the same instance. This allows the user to log in. Of course, a production system would authenticate the user, but it would still follow the same pattern. The user input is captured first in a transient navigation model:

public class NavigationModel
{
    private Independent<string> _userName = new Independent<string>();

    public string UserName
    {
        get { return _userName; }
        set { _userName.Value = value; }
    }
}

This is where the user name is stored while the user is logging in. We expose this property and a login command through the view model.

public string UserName
{
    get { return _navigationModel.UserName; }
    set { _navigationModel.UserName = value; }
}

public ICommand LogIn
{
    get
    {
        return MakeCommand
            .When(() => !String.IsNullOrEmpty(_navigationModel.UserName))
            .Do(() =>
            {
                User user = _community.AddFact(new User(_navigationModel.UserName));
                _navigationModel.CurrentUser = user;
                _navigationModel.UserName = string.Empty;
            });
    }
}

The login command is only enabled when a user name has been entered. When the user clicks the button, a new User is created with the name that they entered. That User is then stored in the navigation model, and the entered name is cleared out.

List tasks

Once we have a User fact in our navigation model, we can list the tasks assigned to that user. A task is another fact, which references the User to which it is assigned.

fact Task {
key:
    unique;
    User assignedTo;
}

Because the key doesn’t already contain enough information to distinguish one task from another, it is marked as “unique”. This causes Correspondence to generate a new Task every time one is created, rather than looking for an existing one.

The User queries for related tasks.

fact User {
    ...

query:
    Task* openTasks {
        Task t : t.assignedTo = this
            where not t.isCompleted
    }
}

That query is exposed through the view model. The MainViewModel generates a list of TaskViewModels to wrap the Tasks.

public IEnumerable<TaskViewModel> OpenTasks
{
    get
    {
        if (_navigationModel.CurrentUser != null)
            return
                from Task t in _navigationModel.CurrentUser.OpenTasks
                select new TaskViewModel(t);
        else
            return Enumerable.Empty<TaskViewModel>();
    }
}

Add a task

The user can enter the name of a task, then hit the Add button to add it to the list. Just like the user name, the task name is stored in the navigation model temporarily. When a user is logged on and a name has been entered, the button becomes enabled. When clicked, it adds a task assigned to the user, and sets the task name.

public ICommand AddTask
{
    get
    {
        return MakeCommand
            .When(() =>
                _navigationModel.CurrentUser != null &&
                !String.IsNullOrEmpty(_navigationModel.TaskName))
            .Do(() =>
            {
                Task task = _community.AddFact(new Task(_navigationModel.CurrentUser));
                task.Name = _navigationModel.TaskName;
                _navigationModel.TaskName = String.Empty;
            });
    }
}

Change a task name

Notice that the constructor above only takes the User to which the task is assigned. That’s because the User is part of the key. The task name is not. It is a mutable property.

fact Task {
    ...

mutable:
    string name;
}

Because the property is mutable, the user can edit the name and hit the Update button. The button is only enabled when a task is selected and the name has been entered.

public ICommand UpdateTask
{
    get
    {
        return MakeCommand
            .When(() =>
                _navigationModel.SelectedTask != null &&
                !String.IsNullOrEmpty(_navigationModel.TaskName))
            .Do(() =>
            {
                _navigationModel.SelectedTask.Name = _navigationModel.TaskName;
                _navigationModel.TaskName = String.Empty;
            });
    }
}

Mark a task completed

Finally, the user can mark a task as completed. This is itself a new fact.

fact TaskCompleted {
key:
    Task completedTask;
}

The Task fact queries for related TaskCompleted facts to see if it is completed.

fact Task {
    ...

query:
    bool isCompleted {
        exists TaskCompleted c : c.completedTask = this
    }
}

This query is then used in the User’s query so that he sees only the open tasks.

fact User {
    ...

query:
    Task* openTasks {
        Task t : t.assignedTo = this
            where not t.isCompleted
    }
}

When the user clicks the Complete button, it completes the task. We added this method to the Task itself using a partial class.

public ICommand CompleteTask
{
    get
    {
        return MakeCommand
            .When(() => _navigationModel.SelectedTask != null)
            .Do(() =>
            {
                _navigationModel.SelectedTask.Complete();
            });
    }
}
public partial class Task
{
    public void Complete()
    {
        Community.AddFact(new TaskCompleted(this));
    }
}

Because this creates the TaskCompleted fact, it causes the list to refresh, and the task is removed.

Build your own apps

Please download Correspondence and try it out. If you have any questions about building your own models, please contact me by email or Twitter. If you’d like to use my synchronization server, just send me a request for an API key. I’ll be glad to help you get set up.

Building an occasionally connected Silverlight application with Correspondence from Michael L Perry on Vimeo.