Archive for May, 2011

Thought Cloud UI #5: Arrange your thoughts

Tuesday, May 31st, 2011

I’ll be presenting Thought Cloud at the Dallas .NET User Group and the North Texas Silverlight User Group in a couple of weeks. I’m finishing up the demo now.

In the last installment, we added the ability to open a cloud in a tab. Now we need to display the thoughts in a cloud of bubbles rather than in a list. To make a plan, I created a mock view.

image

The mock is not backed by sample data. It does not bind to the view models. But it does exercise some XAML that we want to use for the real view. Most significantly, I determined how to position the bubbles.

First, I determined how to position the text within an ellipse. I want the ellipse to be a fixed margin away from the text, but to expand with the text’s length. After playing around with a few variations, the one that worked best for me was to put both the ellipse and the text block in a grid. The text determines the size of the grid, and the grid determines the size of the ellipse.

<Grid>
    <Ellipse />
    <TextBlock Text="Star Wars" Margin="15,10" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>

Then there was the question of positioning the thought bubble within the cloud. My first thought was to use a Canvas. Unfortunately, Canvas lays out its children based on the left and top. I want to position my thought bubbles based on the center. In order to convert center to left and top, I would need to measure width and height. That would require doing the layout in the view. I’m going to try to do everything through data binding in the view model.

What I settled on was using a Grid as a container, centering each thought bubble, and positioning them thought their margins. A centered bubble at {0,0,0,0} will appear in the center of the Grid. When I change the margin to {10,0,-10,0}, it moves to the right by 10 units. So I need to bind the margin to {X, Y, -X,-Y}.

Throw in a text block for editing the thought and this is the item template that I need to bind.

<DataTemplate x:Key="ThoughtItemTemplate">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="{Binding Margin}">
        <Ellipse />
        <TextBlock Text="{Binding Text}" Margin="15,10" HorizontalAlignment="Center" VerticalAlignment="Center" />
        <TextBox Text="{Binding Text, Mode=TwoWay}" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="{Binding Editing, Converter={StaticResource VisibleWhenTrueConverter}}"/>
    </Grid>
</DataTemplate>

Let’s add these properties to the ThoughtViewModel.

public interface ThoughtViewModel
{
    string Text { get; set; }
    Thickness Margin { get; }
    bool Editing { get; }
}

Calculate position

The position of a thought is calculated. In other words, it’s dependent. The user doesn’t get to position it. The tricky part is that it depends upon where the thought falls within the cloud among its neighbors. So a ThoughtViewModel doesn’t have enough information to calculate position. That is the responsibility of the CloudViewModel.

Let’s give the CloudViewModel a dependent dictionary of Points by Thought. We introduce the Dependent class from Update Controls, which tracks dependency using an update method. It will invoke the update method only when the property is out-of-date. In this case, the dictionary depends upon the collection of thoughts.

public class CloudViewModel
{
    ...
    private Dictionary<Thought, Point> _centerByThought = new Dictionary<Thought, Point>();
    private Dependent _depCenterByThought;

    public CloudViewModel(Cloud cloud)
    {
        _depCenterByThought = new Dependent(() => _centerByThought = CalculateCenterByThought());
    }

    private Dictionary<Thought, Point> CalculateCenterByThought()
    {
        Thought centralThought = _cloud.CentralThought;
        Dictionary<Thought, Point> centerByThought = new Dictionary<Thought, Point>();
        if (centralThought != null)
        {
            centerByThought.Add(centralThought, new Point(0.0, 0.0));
            List<Thought> neighbors = centralThought.Neighbors
                .Where(n => n != centralThought)
                .ToList();
            int step = 0;
            double horizontalRadius = 230.0;
            double verticalRadius = 120.0;
            foreach (Thought neighbor in neighbors)
            {
                double theta = 2 * Math.PI * (double)step / (double)neighbors.Count;
                centerByThought.Add(neighbor, new Point(
                    horizontalRadius * Math.Cos(theta),
                    verticalRadius * Math.Sin(theta)));
                ++step;
            }
        }
        return centerByThought;
    }
}

Because the update method gets the neighbors of the central thought, it will become out-of-date whenever a neighbor is added or removed, or the central thought is changed.

Next we need to give the ThoughtViewModel a way to access its own center point. Let’s pass a function into its constructor. This function will look up the thought’s center point from the dictionary. The call to OnGet will ensure that the dictionary is up-to-date.

public class CloudViewModel
{
    ...

    public IEnumerable<ThoughtViewModel> Thoughts
    {
        get
        {
            Thought centralThought = _cloud.CentralThought;
            if (centralThought == null)
            {
                return Enumerable.Repeat(new ThoughtViewModelSimulated(_cloud), 1)
                    .OfType<ThoughtViewModel>();
            }
            else
            {
                return Enumerable.Repeat(new ThoughtViewModelActual(centralThought, GetCenterByThought), 1).Union(
                    from n in centralThought.Neighbors
                    where n != centralThought
                    select new ThoughtViewModelActual(n, GetCenterByThought))
                    .OfType<ThoughtViewModel>();
            }
        }
    }

    private Point GetCenterByThought(Thought thought)
    {
        _depCenterByThought.OnGet();
        Point center = new Point();
        _centerByThought.TryGetValue(thought, out center);
        return center;
    }
}

Now the ThoughtViewModel can calculate its own Margin.

public class ThoughtViewModelActual : ThoughtViewModel
{
    ...
    private readonly Func<Thought, Point> _getCenterByThought;

    public ThoughtViewModelActual(Thought thought, Func<Thought, Point> getCenterByThought)
    {
        _getCenterByThought = getCenterByThought;
    }

    public Thickness Margin
    {
        get
        {
            Point center = _getCenterByThought(_thought);
            return new Thickness(center.X, center.Y, -center.X, -center.Y);
        }
    }
}

By using a dependent dictionary, the cloud can calculate the position of all of its thoughts once, and let each thought access it to data bind into place. When a new thought arrives, either from the local user or from a remote collaborator, the positioning will be recalculated and all thoughts will move to their new locations.

Thought Cloud

Thought Cloud UI #4: Open the clouds

Monday, May 30th, 2011

The Thought Cloud example will be ready just in time for my talk at the Dallas .NET User Group. I hope you’ll join me.

We’ve created a very simple home view that shows a list of clouds and has an add button. Next we’ll need to open a cloud and show it in a separate view. Remember how we keep track of what the user has opened? That’s right: the navigation model.

public class NavigationModel
{
    ... 
    private IndependentList<Cloud> _openClouds = new IndependentList<Cloud>();

    public IEnumerable<Cloud> OpenClouds
    {
        get { return _openClouds; }
    }

    public void OpenCloud(Cloud cloud)
    {
        if (!_openClouds.Contains(cloud))
            _openClouds.Add(cloud);
    }
}

We keep an IndependentList of opened clouds. This is kind of like an ObservableCollection, but it works with dependency tracking. Whenever a cloud is added or removed, all dependent properties that reference this list will be updated.

To add a cloud to the list, we need to know which one the user has selected. So we add that to the navigation model too.

public class NavigationModel
{
    ...

    private Independent<Cloud> _selectedCloud = new Independent<Cloud>();

    public Cloud SelectedCloud
    {
        get { return _selectedCloud; }
        set { _selectedCloud.Value = value; }
    }
}

And then we need to data bind that to the SelectedItem of the list box. Since the list box shows CloudSummaryViewModels, the SelectedItem needs to be of that type as well.

public class HomeViewModel
{
    ...

    public CloudSummaryViewModel SelectedCloud
    {
        get
        {
            return _navigation.SelectedCloud == null
                ? null
                : new CloudSummaryViewModel(_navigation.SelectedCloud);
        }
        set
        {
            _navigation.SelectedCloud = value == null
                ? null
                : value.Cloud;
        }
    }
}

Now we have all the pieces to create the OpenCloud command.

public class HomeViewModel
{
    ...

    public ICommand OpenCloud
    {
        get
        {
            return MakeCommand
                .When(() => _navigation.SelectedCloud != null)
                .Do(() => _navigation.OpenCloud(_navigation.SelectedCloud));
        }
    }
}

Data bind the SelectedItem of the cloud list and an “Open Cloud” button in the home view. Now the user can select a cloud and open it. Nothing happens, yet, but it’s added to the navigation model.

Tab through the open clouds

Next we want a tab control that displays all of the open clouds. So we’ll need a view model for a tab. This will determine the tab header and content.

public class CloudTabViewModel
{
    private readonly Cloud _cloud;

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

    internal Cloud Cloud
    {
        get { return _cloud; }
    }

    public string Header
    {
        get
        {
            Thought centralThought = _cloud.CentralThought;
            if (centralThought == null)
                return "<New cloud>";
            string text = centralThought.Text;
            if (text == null)
                return "<New cloud>";
            return text;
        }
    }

    public CloudViewModel Content
    {
        get { return new CloudViewModel(_cloud); }
    }
}

The content, of course, will be a CloudView. We’ll build that user control next time. For now, we add the open clouds to the MainViewModel.

public class MainViewModel
{
    ...

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

Add a tab control to the MainPage and bind ItemsSource to OpenClouds. This should work, but Silverlight throws an exception.

Unable to cast object of type 'UpdateControls.XAML.Wrapper.ObjectInstance`1[FacetedWorlds.ThoughtCloud.ViewModel.CloudTabViewModel]' to type 'System.Windows.Controls.TabItem'.

Amanda Wang provides the solution. We’ve given the view a list of CloudTabViewModels (which Update Controls has wrapped for dependency tracking), but it expects a list of TabItems. We need to convert them.

I created a value converter based on Amanda’s proposed solution, but I made it specific to Update Controls. It data binds the Header and Content properties to the source object. It also expects an ObservableCollection, so that changes to the collection result in changes to tabs. See the source code for details.

Now when a user selects a cloud from the list and hits the open button, the cloud is added to the navigation model. The tabs depend upon the navigation model, so the new tab is opened.

Thought Cloud UI #3: Synchronization status

Thursday, May 26th, 2011

The Thought Cloud demo is on GitHub. Fork it and follow along.

We just got an application limping along. It hard-codes the user, and it displays a list of clouds. We can see that isolated storage is working for us, because we can close the application and our clouds don’t disappear. But how do we know if the communication strategy is working?

You can report the status of the communications directly to your user through data binding. Community has two properties designed for this:

  • Synchronizing
  • LastException

These two properties are Independent, so you can data bind them through any view model. Let’s add these to the MainViewModel.

public bool Synchronizing
{
    get { return _community.Synchronizing; }
}

public bool HasError
{
    get { return _community.LastException != null; }
}

public string LastError
{
    get
    {
        return _community.LastException == null
            ? null
            : _community.LastException.Message;
    }
}

Then we can data bind these properties to some controls. The busy indicator will be visible when Synchronizing is true. The error indicator will be visible when HasError is true. And the text of the error indicator will be the LastError string. To help us convert boolean to Visibility, let’s add a reference to Itzben. It contains a useful little VisibleWhenTrueConverter. Itzben is available on NuGet.

image

When we run the app, we see a problem. The error message says: “Domain <<Your API key>> not registered.” We have to use a real Correspondence API key. If you want to use my Correspondence server, please email or mention me for a key. I am letting people use the server free for a limited time.

public class POXConfigurationProvider : IPOXConfigurationProvider
{
    public POXConfiguration Configuration
    {
        get
        {
            string address = "https://api.facetedworlds.com:9443/correspondence_server_web/pox";
            string apiKey = "B22E33EB0ABD46FE9161BF4FB8748A65";
            return new POXConfiguration(address, "FacetedWorlds.ThoughtCloud", apiKey);
        }
    }
}

Once we put the real API key in place, the service works. It sends new clouds to the server. If you run the application on a different machine, it will pull those clouds back down from the server.

Thought Cloud UI #2: Summary view models

Wednesday, May 25th, 2011

We’ve started building the Thought Cloud user interface. It won’t be long until we see something working.

image I like to build Silverlight user interfaces via composition. I construct each view as a separate UserControl. Then I compose them into a working application. Taking this approach, let’s create a Views folder in the application, and a HomeView UserControl.

I use Visual Studio to cerate all of my user controls, since Blend messes up the namespaces. But once it’s created, I switch to Blend for everything else.

Create sample data

The HomeView will be data bound to the HomeViewModel. The first step is to create sample data from that class. This will give us all of the properties that we can data bind to, and it will let Blend automatically generate some very useful things.

image

image

The home view model simply has a list of clouds and a command to add a new cloud. Each cloud in the list has a list of thoughts and a command to add a new thought. There’s a problem with that. On this view, we intend to show a summary of each cloud. The next view will let you edit thoughts. Let’s change our cloud view models to cloud summary view models.

public class CloudSummaryViewModel
{
    private Cloud _cloud;

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

    internal Cloud Cloud
    {
        get { return _cloud; }
    }

    public string Text
    {
        get
        {
            Thought centralThought = _cloud.CentralThought;
            if (centralThought == null)
                return "<New cloud>";
            string text = centralThought.Text;
            if (text == null)
                return "<New cloud>";
            return text;
        }
    }

    public override bool Equals(object obj)
    {
        if (obj == this)
            return true;
        CloudSummaryViewModel that = obj as CloudSummaryViewModel;
        if (that == null)
            return false;
        return _cloud == that._cloud;
    }

    public override int GetHashCode()
    {
        return _cloud.GetHashCode();
    }
}

The cloud summary view model displays the text of the central thought. If it has no central thought, or if the text is not yet initialized, then it displays “<New cloud>”. This is different than the “My thought” behavior that we implemented in the cloud view model. When the user is looking at a list of clouds, they might get confused if one of them claims to be a thought.

The cloud summary view model also returns the wrapped Cloud object, and implements Equals and GetHashCode. This pattern always arises for a view model that appears in a selectable list. The Cloud property let’s us correlate user selection with the model (it is internal so that we don’t accidentally data bind to it). And the Equals and GetHashCode methods make sure that the user selection is equal to a member of the list, even if a new view model is created.

Regenerate sample data

Since we changed the view model after generating the sample data, Blend warns us that it is incorrect.

image

imageThe easiest thing to do is delete it and recreate it. Then we can drag the Clouds property onto the art board. We end up with a list box filled with lorem ipsum text for several clouds.

What Blend has done is to create an item template that brings in the item properties. In this case, the list contains CloudSummaryViewModels, which has only the Text property. (Remember that we made the Cloud property internal so Blend would ignore it.) To see the generated item template, right-click the list, choose “Edit Additional Templates”, “Edit Generated Items (ItemTemplate)”, “Edit Current”.

image

This will dive into the list box and show you an item template with just one TextBlock. It is bound to the Text property.

Compose the application

To see the application running, we need to do a couple more things. First, drop a button on the HomeView and drag the AddCloud command onto it. This data binds the button to the command. Then open the MainPage and add a HomeView to the page. You can get to the HomeView by clicking the “Assets” toolbar button and selecting “Project”.

image

Now we need to give the view a view model. Recall from the last post that MainPage uses a view model locator to find a MainViewModel. So we can just expose a HomeViewModel from MainViewModel.

public HomeViewModel Home
{
    get
    {
        return _navigationModel.CurrentUser == null
            ? null
            : new HomeViewModel(_navigationModel.CurrentUser);
    }
}

This property will give us a home view model if the user is logged in. If not, it will return null. When the user logs in or out, CurrentUser changes (remember that it is Independent), so the view model will fire PropertyChanged. We haven’t implemented the log in logic, so let’s just fake it for now.

public class SynchronizationService
{
    ...

    public void Initialize()
    {
        ...

        _navigationModel.CurrentUser = _community.AddFact(new Identity("mike"));
    }
}

All that remains is to data bind the DataContext of the HomeView inside of the MainPage to the Home property. Generate sample data for MainViewModel, and drag the Home property onto the HomeView. We now have an app that will add “<New Cloud>” to a list box whenever you hit the “New Cloud” button.

Thought Cloud UI #1: Structure of the UI project

Tuesday, May 24th, 2011

The TDD phase of Thought Cloud is done (for now). Now we begin building the user interface.

When I set up the project structure, I added the NuGet package “Correspondence.Silverlight.App” to a Silverlight application. In addition to adding a reference to the Correspondence and Update Controls assemblies, this added a few features to the app:

  • View Model Locator
  • Synchronization Service
  • Navigation Model

View Model Locator

I am not a big fan of the View Model Locator pattern. I believe that it is backwards. A view should not locate its view model: a view model should be injected into a view. Nevertheless, Silverlight’s architecture makes it easier to work with the VML than against it. So I’ll fight that battle another day.

The ViewModelLocator class created by the NuGet package exposes a MainViewModel. It wraps the view model for the view using Update Controls’ ForView.Wrap() method. This sets up all of the property changed notification through automatic dependency tracking.

public class ViewModelLocator
{
    private readonly SynchronizationService _synchronizationService;

    private readonly MainViewModel _main;

    public ViewModelLocator()
    {
        NavigationModel navigationModel = new NavigationModel();
        _synchronizationService = new SynchronizationService(navigationModel);
        if (!DesignerProperties.IsInDesignTool)
            _synchronizationService.Initialize();
        _main = new MainViewModel(_synchronizationService.Community, navigationModel, _synchronizationService);
    }

    public object Main
    {
        get { return ForView.Wrap(_main); }
    }
}

To use the view model locator, we follow the instructions in Readme.txt. We need to add it to application resources, and reference it from the main page.

In addition, the view model locator creates the synchronization service and navigation model, and passes those into the main view model.

Synchronization Service

The SynchronizationService class is responsible for synchronizing the local data storage with the remote service. As part of that responsibility, it sets up the Community. It uses the IsolatedStorageStorageStrategy to persist facts locally, and the POXAsynchronousCommunicationStrategy to exchange those facts with the server.

public class SynchronizationService
{
    ...

    public void Initialize()
    {
        POXConfigurationProvider configurationProvider = new POXConfigurationProvider();
        _community = new Community(IsolatedStorageStorageStrategy.Load())
            .AddAsynchronousCommunicationStrategy(new POXAsynchronousCommunicationStrategy(configurationProvider))
            .Register<CorrespondenceModel>()
            .Subscribe(() => _navigationModel.CurrentUser)
            .Subscribe(() => _navigationModel.CurrentUser.SharedClouds)
            .Subscribe(() => _navigationModel.CurrentUser.Clouds)
            ;

        // Synchronize whenever the user has something to send.
        _community.FactAdded += delegate
        {
            Synchronize();
        };

        // And synchronize on startup.
        Synchronize();
    }

    ...
}

During testing, we found which subscriptions need to be set up. We add those subscriptions here. The synchronization service subscribes to the current user as provided by the navigation model.

Navigation Model

The NavigationModel class is responsible for maintaining the user temporary state. This state represents the user’s point-of-view as they navigate through the system. Typical things that you will find in a navigation model are:

  • Selected items
  • Search terms
  • Open windows
  • User session

These things are all important to the user, but not actually part of the model. They are transient, and don’t get persisted to storage or shared with other users.

public class NavigationModel
{
    private Independent<Identity> _currentUser = new Independent<Identity>();

    public Identity CurrentUser
    {
        get { return _currentUser; }
        set { _currentUser.Value = value; }
    }
}

Fields of a navigation model are independent, meaning that the user can change them directly. We have to mark them using the Update Controls Independent modifier. Dependent behaviors, like view model properties and subscriptions, will respond to changes in independent fields. So when a user logs on and CurrentUser is set, not only will the view model update (and fire the appropriate PropertyChanged events), but the Community subscriptions will refresh as well.

With these three components generated for us, we can start assembling the user interface of the application.

Thought Cloud TDD #10: Think back

Tuesday, May 24th, 2011

We’re just about done with the TDD push on Thought Cloud. This is the last test before creating some UI.

In the last test, Mike published a thought to Russell. Let’s see if Russell can send one back to Mike.

[TestMethod]
public void RussellCanShareAThoughtWithMike()
{
    Cloud cloud = MikeSharesCloudWithRussell();
    Thought thought = cloud.NewThought();
    cloud.CentralThought = thought;
    thought.Text = "Lunch suggestions";

    Synchronize();

    Cloud sharedCloud = _russellsIdentity.SharedClouds.Single();
    Thought newThought = sharedCloud.NewThought();
    newThought.Text = "Mi Pueblo";
    Thought centralThought = sharedCloud.CentralThought;
    centralThought.LinkTo(newThought);

    Synchronize();

    IEnumerable<Thought> suggestions = thought.Neighbors.Where(n => n != thought);
    Assert.AreEqual(1, suggestions.Count());
    string suggestionText = suggestions.Single().Text;
    Assert.AreEqual("Mi Pueblo", suggestionText);
}

In this test, Mike shares a cloud with the central thought “Lunch suggestions” with Russell. Russell responds by creating a linked thought with the text “Mi Pueblo”. Does Mike see the response?

No! The test fails. Can you see why?

The reason is that we have subscribed to all clouds shared with us. We haven’t subscribed to the clouds that we created ourselves.

_mikesCommunity = new Community(new MemoryStorageStrategy())
    .AddCommunicationStrategy(sharedCommunication)
    .Register<Model.CorrespondenceModel>()
    .Subscribe(() => _mikesIdentity)
    .Subscribe(() => _mikesIdentity.SharedClouds)
    .Subscribe(() => _mikesIdentity.Clouds)
    ;

With this new subscription, the test passes. Mike created the cloud, so now he subscribes to it. When Russell adds a thought to it, that thought is published to Mike’s cloud.

Thought Cloud TDD #9: Penny for your thoughts

Monday, May 23rd, 2011

The Thought Cloud application just needs a few more tests. Then we are on to building the UI.

In the last test, we made it possible to share clouds with other users. But what we really want to do is share thoughts.

[TestMethod]
public void MikeCanShareThoughtsWithRussell()
{
    Cloud cloud = MikeSharesCloudWithRussell();
    Thought thought = cloud.NewThought();
    cloud.CentralThought = thought;
    thought.Text = "Lunch suggestions";

    Synchronize();

    Thought sharedThought = _russellsIdentity.SharedClouds.Single().CentralThought;
    string sharedText = sharedThought.Text;
    Assert.AreEqual("Lunch suggestions", sharedText);
}

In this test, Mike is sharing the cloud with Russell, and then adding a thought. Russell should be able to see the thought and the text that Michael put into it. The test, of course, fails.

Subscribe to related facts

The test fails because Russell thinks that CentralThought is still null. The Share was published to him, but not the CentralThought. Let’s publish that now.

fact Cloud {
key:
    unique;
    Identity creator;

mutable:
    publish Thought centralThought;
}

This keyword publishes the CentralThought property to the Cloud. So to receive it, Russell must subscribe to the cloud.

_russellsCommunity = new Community(new MemoryStorageStrategy())
    .AddCommunicationStrategy(sharedCommunication)
    .Register<Model.CorrespondenceModel>()
    .Subscribe(() => _russellsIdentity)
    .Subscribe(() => _russellsIdentity.SharedClouds)
    ;

With this subscription, Russell receives all facts published to the clouds that have been shared with him. This should be enough to let him see CentralThought. But the test still fails.

Publish successors to shared facts

The test fails now because Russell thinks that the central thought’s text is still null. He can’t see that Mike has changed it. So we somehow need to publish the thought’s text to the Cloud.

Text is a mutable field of a Thought, so it can be published to a Thought. It cannot be published directly to a Cloud. But a Thought can be published to a Cloud.

fact Thought {
key:
    unique;
    publish Cloud cloud;

...
}

This change makes the unit test pass. Why? Because of rule #5.

Rule #5: All successors of published facts are automatically published.

The Text property of the central thought is a successor of the Thought. The Thought is published to the Cloud. Therefore, a subscriber to the Cloud will also see the Text.

Thought Cloud TDD #8: Hey, you, get onto my cloud

Monday, May 23rd, 2011

This Thought Cloud application is going to be a lot of fun. It’s about to get really exciting.

What good is a collaborative mind mapper if you can’t share thoughts with other people? It’s time to bring on the collaboration tests. The ModelTest creates two communities representing two user’s machines: Mike and Russell.

[TestInitialize]
public void Initialize()
{
    var sharedCommunication = new MemoryCommunicationStrategy();
    _mikesCommunity = new Community(new MemoryStorageStrategy())
        .AddCommunicationStrategy(sharedCommunication)
        .Register<Model.CorrespondenceModel>()
        .Subscribe(() => _mikesIdentity)
        ;
    _russellsCommunity = new Community(new MemoryStorageStrategy())
        .AddCommunicationStrategy(sharedCommunication)
        .Register<Model.CorrespondenceModel>()
        .Subscribe(() => _russellsIdentity)
        ;

    _mikesIdentity = _mikesCommunity.AddFact(new Identity("mike"));
    _russellsIdentity = _russellsCommunity.AddFact(new Identity("russell"));
}

The communities are joined by a shared communication strategy. That means that facts created in one community can flow into the other. In order to trigger this flow, we have to call Synchronize.

private void Synchronize()
{
    while (_mikesCommunity.Synchronize() || _russellsCommunity.Synchronize()) ;
}

The Synchronize method continues to push facts between the two communities until neither one has facts to share. We can use this to set up tests. The first test is that if Mike shares a cloud with Russell, Russell can see the cloud.

[TestMethod]
public void MikeCanShareCloudWithRussell()
{
    Cloud cloud = _mikesIdentity.NewCloud();
    Identity russell = _mikesCommunity.AddFact(new Identity("russell"));
    russell.NewShare(cloud);

    Synchronize();

    Assert.AreEqual(1, _russellsIdentity.SharedClouds.Count());
    Assert.AreEqual("mike", _russellsIdentity.SharedClouds.Single().Creator.AnonymousId);
}

Mike adds a fact to his community with the id of “russell”. This fact matches the one in Russell’s community, so it will be treated as the same fact when the two synchronize. He then shares his cloud with Russell. After they synchronize, Russell should see the shared cloud created by Mike.

A shared fact

To make this test pass, we need to define a fact that shares a cloud with an identity.

fact Share {
key:
    Identity recipient;
    Cloud cloud;
}

Then we need to query an identity for all of the shared clouds.

fact Identity {
...

query:

    Cloud* sharedClouds {
        Share s : s.recipient = this
        Cloud c : s.cloud = c
    }
}

That query combines two sets. First, the set of all shares to this recipient. Then, the set of all clouds thus shared. Finally, we need a method in our partial class to create a new share.

public partial class Identity
{
    ...

    public void NewShare(Cloud cloud)
    {
        Community.AddFact(new Share(this, cloud));
    }
}

With these changes, the test should pass. But it doesn’t. Why not?

Publish shared facts

If you look closely at the setup of each Community, you’ll see a subscription. Here it is again:

_russellsCommunity = new Community(new MemoryStorageStrategy())
    .AddCommunicationStrategy(sharedCommunication)
    .Register<Model.CorrespondenceModel>()
    .Subscribe(() => _russellsIdentity)
    ;

That line says that Russell’s community is only interested in facts that are published to Russell’s identity. So to get the Share over to the other community, we need to publish it to the Identity.

fact Share {
key:
    publish Identity recipient;
    Cloud cloud;
}

And now the test passes.

Rule #4: A community only receives published facts to which it subscribes.

Wait a minute! The test asserts that Russell can see that the cloud was created by Mike. How can Russell see Mike’s identity if it wasn’t published?

Before a fact is sent, all of its predecessors must first be sent. That means that in order to receive a Share, Russell must first receive the Cloud that it references. Before he can receive the Cloud, he must receive the Identity of the creator.

Rule #4.1: … and all of their predecessors.

A community doesn’t receive all facts. It only receives what it subscribes to. You need to manage the publications and subscriptions. Once you do, two Communities can collaborate through a shared synchronization strategy.

Thought Cloud TDD #7: Devoid of thought

Friday, May 20th, 2011

I’m writing Thought Cloud for a demo in a couple of weeks. Help me out.

During the last refactoring, we introduced a bug. There just isn’t a test to show it yet. The bug is related to rule number two:

Rule #2: Create one fact for each user action.

In the last refactoring, a single user action created three facts: a cloud, and thought, and a central thought assignment. (You can’t always see it, but an assignment to a mutable field is a new fact.) The problem with creating three facts for a single user action is that we cannot guarantee that all three facts are atomic. There may be situations – particularly when collaborating with another user – where we see only a subset of the facts.

Honor the default state

After each fact that we create, we must assume that the system is in a valid state. In this scenario, we have two intermediate states which must be considered valid:

  • The cloud has been created, but it has no thoughts.
  • The cloud has a thought, but no central thought.

These are the states that arise after the creation of the first and second facts. We must honor the default state of a cloud. Let’s drop the initialization steps from the unit test and see what happens:

[TestInitialize]
public void Initialize()
{
    _community = new Community(new MemoryStorageStrategy())
        .Register<Model.CorrespondenceModel>();

    _identity = _community.AddFact(new Identity("mike"));
    Cloud cloud = _community.AddFact(new Cloud(_identity));
    //Thought thought = _community.AddFact(new Thought(cloud));
    //cloud.CentralThought = thought;
    _cloudViewModel = new CloudViewModel(cloud);
}

Now all three of the tests in this suite fail. They all throw null reference exceptions. The first null reference is in the Thoughts property of the CloudViewModel. The cloud has no central thought. In this situation, we want to simulate a thought to make the tests pass.

public IEnumerable<ThoughtViewModel> Thoughts
{
    get
    {
        Thought centralThought = _cloud.CentralThought;
        if (centralThought == null)
        {
            return Enumerable.Repeat(new ThoughtViewModelSimulated(_cloud), 1)
                .OfType<ThoughtViewModel>();
        }
        else
        {
            return Enumerable.Repeat(new ThoughtViewModelActual(centralThought), 1).Union(
                from n in centralThought.Neighbors
                where n != centralThought
                select new ThoughtViewModelActual(n))
                .OfType<ThoughtViewModel>();
        }
    }
}

If the central thought has not been set, then we return a simulated thought view model. If it has, then we return actual thought view models. Those calls to OfType are there because an IEnumerable of a derived type can’t be converted to an IEnumerable of the base type. Covariance will solve this problem, once it is available in Silverlight.

When the user sets the thought text, the simulated thought view model needs a place to store it. So at that point, it creates a thought.

public class ThoughtViewModelSimulated : ThoughtViewModel
{
    private Cloud _cloud;

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

    public string Text
    {
        get { return "My thought"; }
        set
        {
            Thought thought = _cloud.NewThought();
            _cloud.CentralThought = thought;
            thought.Text = value;
        }
    }
}

The last null reference exception occurs in the NewThought command of CloudViewModel. It tries to link the new thought to the central thought, but the central thought is null. We must honor this situation, and create a new central thought when needed.

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

Now the CloudViewModel honors the default state of the Cloud fact. It is valid to have a Cloud with no central thought. Just as the ThoughtViewModel returns the simulated text “My thought” when the Text property has not yet been set, the CloudViewModel returns a simulated thought when no central thought has been created. When the user makes a change, the actual Thought is created. But not before.

Thought Cloud TDD #6: Get your thoughts in order

Friday, May 20th, 2011

I found it backwards that you have to create a thought before you create a cloud.

public ICommand AddCloud
{
    get
    {
        return MakeCommand
            .Do(delegate
            {
                Thought thought = _identity.NewThought();
                _identity.NewCloud(thought);
            });
    }
}

A thought is part of a cloud, so the cloud should be a predecessor. But since we made a thought part of the cloud’s key, the thought is a predecessor.

Rule #3: Facts that are part of the key (a.k.a predecessors) must be created first.

It makes more sense if we create a cloud first, and then add thoughts to it. A cloud should be a predecessor of a thought. Let’s make that change.

fact Thought {
key:
    unique;
    Cloud cloud;

...
}

Now we need to create a thought in a Cloud. Let’s move the NewThought method from Identity to Cloud where it belongs.

public partial class Cloud
{
    public Thought NewThought()
    {
        return Community.AddFact(new Thought(this));
    }
}

Now we can create the cloud first.  But hold on! The cloud needs a central thought. The central thought is in the cloud. That’s a circular reference. The predecessor rule makes it impossible to create circular references out of keys. This is an important feature of Correspondence.

But a circular reference can be constructed if one of the fields is mutable. So let’s change the central thought.

fact Cloud {
key:
    unique;
    Identity creator;

mutable:
    Thought centralThought;
}

Now that the central thought is mutable, a cloud can be constructed with no central thought. Then the thought is created, and finally it is assigned.

public ICommand AddCloud
{
    get
    {
        return MakeCommand
            .Do(delegate
            {
                Cloud cloud = _identity.NewCloud();
                Thought thought = cloud.NewThought();
                cloud.CentralThought = thought;
            });
    }
}

This makes more sense. Now thoughts belong to a cloud, not the other way around. After following the ripples out to the edges of the pond, we can compile and pass the unit tests. But we’ve introduced a bug. We just need to write a test to prove it.