Archive for June, 2011

Thought Cloud demo launch

Friday, June 3rd, 2011

The Thought Cloud demo is up at Q.E.D. Code. I will be presenting this demo at:

You can download the source code from GitHub. Follow the creation of this demo step-by-step:

To create your own Correspondence application for Silverlight, add a NuGet reference to Correspondence.Silverlight.AllInOne. The project is hosted on CodePlex. Contact me at mperry@mallardsoft.com or @michaellperry for an API key for the Correspondence server.

Thought Cloud TDD #11: Conflict detection

Thursday, June 2nd, 2011

The Thought Cloud application is just about ready to release. Get the code now, or watch this space for a live deployment.

An occasionally-connected client application works off line as well as on. While off line, it queues changes that the user makes. It synchronizes those changes when the connection is reestablished. So the question arises: what happens if someone made a conflicting change while I was off line?

Correspondence performs conflict detection. Conflict resolution is up to your application.

Every mutable field is represented in code with the Disputable<T> type. This type implicitly converts to T. The Text property of a Thought is a mutable string.

fact Thought {
key:
    unique;
    publish Cloud cloud;

mutable:
    string text;

    ...
}

This generates to a Disputable<string>, which the view model treats as a regular string.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...

    public string Text
    {
        get { return _thought.Text ?? "My thought"; }
        set
        {
            _thought.Text = value;
            _container.EditThought = null;
        }
    }
}

Disputable<T> has a Value property which can be used in place of implicit type conversion. It also has two extra properties:

  • InConflict – true when a conflict is detected.
  • Candidates – the collection of possible values.

With these two properties, your application can present conflicts to the user.

[TestMethod]
public void ConflictsAreDetected()
{
    Cloud cloud_Mike = MikeSharesCloudWithRussell();
    Thought thought_Mike = cloud_Mike.NewThought();
    cloud_Mike.CentralThought = thought_Mike;
    thought_Mike.Text = "Initial value";

    Synchronize();

    Thought thought_Russell = _russellsIdentity.SharedClouds.Single().CentralThought;
    thought_Mike.Text = "Mike's change";
    thought_Russell.Text = "Russell's change";

    Synchronize();

    Assert.IsTrue(thought_Mike.Text.InConflict);
    Assert.IsTrue(thought_Mike.Text.Candidates.Contains("Mike's change"));
    Assert.IsTrue(thought_Mike.Text.Candidates.Contains("Russell's change"));
    Assert.IsFalse(thought_Mike.Text.Candidates.Contains("Initial value"));

    Assert.IsTrue(thought_Russell.Text.InConflict);
    Assert.IsTrue(thought_Russell.Text.Candidates.Contains("Mike's change"));
    Assert.IsTrue(thought_Russell.Text.Candidates.Contains("Russell's change"));
    Assert.IsFalse(thought_Russell.Text.Candidates.Contains("Initial value"));
}

To resolve a conflict, simply assign the field to the desired value. It does not necessarily have to be one of the candidates.

[TestMethod]
public void ConflictsCanBeResolved()
{
    Cloud cloud_Mike = MikeSharesCloudWithRussell();
    Thought thought_Mike = cloud_Mike.NewThought();
    cloud_Mike.CentralThought = thought_Mike;
    thought_Mike.Text = "Initial value";

    Synchronize();

    Thought thought_Russell = _russellsIdentity.SharedClouds.Single().CentralThought;
    thought_Mike.Text = "Mike's change";
    thought_Russell.Text = "Russell's change";

    Synchronize();

    thought_Mike.Text = "Mike's resolution";

    Synchronize();

    Assert.IsFalse(thought_Mike.Text.InConflict);
    Assert.AreEqual("Mike's resolution", thought_Mike.Text.Value);

    Assert.IsFalse(thought_Russell.Text.InConflict);
    Assert.AreEqual("Mike's resolution", thought_Russell.Text.Value);
}

You can leave conflict resolution to the user. Expose these properties through the data model to give the user a visual indicator of the conflict. When they set the value, they will resolve the conflict.

Thought Cloud UI #6: Change your mind

Wednesday, June 1st, 2011

The Thought Cloud demo is almost feature complete. Just a few more changes.

The application is not very interactive yet. The user should be able to switch focus from one thought to another. They should also be able to edit thought text. Let’s take these features one at a time.

When you think about a user switching their focus from one object to another, what word comes to mind? If you said “navigation”, then we are on the same page. As the user switches focus from one thought to another, they are navigating through the cloud. They aren’t changing the cloud itself. They are only changing their own point-of-view. We need a place to store that point-of-view.

public class CloudNavigationModel
{
    private readonly Cloud _cloud;

    private Independent<Thought> _focusThought = new Independent<Thought>();

    public CloudNavigationModel(Cloud cloud)
    {
        _cloud = cloud;
    }

    public Thought FocusThought
    {
        get { return _focusThought.Value ?? _cloud.CentralThought.Value; }
        set { _focusThought.Value = value; }
    }
}

The focus thought is independent. The user can change it whenever they want. If they haven’t set it yet, then it defaults to the central thought of the cloud. We don’t just initialize the focus thought to the central thought, because this is another example of honoring the default value. The default of null is a valid value, and should indicate that we want to focus on the cloud’s central thought. As an added bonus, if the cloud’s central thought changes after the navigation model is initialized, the focus thought will shift as well.

Like any navigation model, the cloud navigation model should be injected into the view model. We’ll do that in the MainViewModel, where the cloud view models are constructed.

public IEnumerable<CloudTabViewModel> OpenClouds
{
    get
    {
        return
            from cloud in _navigationModel.OpenClouds
            select new CloudTabViewModel(cloud, new CloudNavigationModel(cloud));
    }
}

Now each cloud view model has its own navigation model. We can easily switch all of the positioning code from the central thought to the focus thought. But how do we change the focus thought? The user should switch focus when they click on a thought. So we add a command to the ThoughtViewModel.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...

    public ICommand Focus
    {
        get
        {
            return MakeCommand
                .Do(() =>
                {
                    if (_container.FocusThought != _thought)
                        _container.FocusThought = _thought;
                });
        }
    }
}

Then we can just use InvokeDataCommand to execute this command on click. When the user clicks on a thought bubble, the focus thought is changed. This causes the positioning to go out-of-date, and all of the thought bubbles to shuffle.

Edit your thoughts

The user needs to change the text of a thought in two circumstances. First, when they have just added the thought. And second, when they click on it. Since clicking on a thought already makes it the focus, we’ll say that clicking on the focus thought puts it in edit mode.

Only one thought at a time can be in edit mode, so we’ll store that edit thought in the navigation model.

public class CloudNavigationModel
{
    ...

    private Independent<Thought> _editThought = new Independent<Thought>();

    public Thought EditThought
    {
        get { return _editThought; }
        set { _editThought.Value = value; }
    }
}

The view model needs a property that will determine when the text box is visible. It will be true when this thought is the edit thought.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...

    public bool Editing
    {
        get { return _container.EditThought == _thought; }
    }
}

When the user clicks the thought bubble, it enters edit mode if it is already the focus thought. So let’s add that to the Focus command.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...

    public ICommand Focus
    {
        get
        {
            return MakeCommand
                .Do(() =>
                {
                    if (_container.FocusThought != _thought)
                        _container.FocusThought = _thought;
                    else if (_container.EditThought != _thought)
                        _container.EditThought = _thought;
                    else
                        _container.EditThought = null;
                });
        }
    }
}

When the user clicks on the background, we want to exit edit mode. So we bind a command to the click event of the background border.

public class CloudViewModel : IThoughtContainer
{
    ...

    public ICommand ClearEdit
    {
        get
        {
            return MakeCommand
                .Do(() => _navigation.EditThought = null);
        }
    }
}

Finally, when the user adds a new thought, we want to put that thought into edit mode. So we add that to the NewThought command.

public class CloudViewModel : IThoughtContainer
{
    ...

    public ICommand NewThought
    {
        get
        {
            return MakeCommand.Do(() =>
            {
                Thought thought = _cloud.NewThought();
                Thought focusThought = _navigation.FocusThought;
                if (focusThought == null)
                {
                    focusThought = _cloud.NewThought();
                    _cloud.CentralThought = focusThought;
                }
                focusThought.LinkTo(thought);
                _navigation.EditThought = thought;
            });
        }
    }
}

With the Navigation Model pattern, we can keep information about the user’s point-of-view. Several different view models can interact with and respond to this navigation model. They don’t need to pass messages to each other. It’s a simple way to help the user browse and edit their information.