The adventure continues

October 26th, 2011

Please follow me over to qedcode.com.

Remembering Steve

October 6th, 2011

My adventure, like so many others of my generation, started on a plastic box built by two guys named Steve in a garage in Los Altos, California.

My sister came home from the first day of high school and told us about these amazing machines that she was working with. You could give these things simple instructions, and they would follow them. Put enough of these instructions together, and it does whatever you want. The next time I went to the mall department store, I saw one of these computers. I played with it until I had a square bouncing around the screen. It went something like this:

10 x = 0
20 y = 0
30 dx = 1
40 dy = 1
50 putchar(" ", x, y)
60 x = x + dx
70 y = y + dy
80 putchar("*", x, y)
90 if x <= 0 or x >= 39 then dx = -dx
100 if y <= 0 or y >= 39 then dy = -dy
110 goto 50

The syntax is wrong, but the spirit is right. I typed in variations of this program on several subsequent visits. I added gravity at some point, but I never saved the program. I always typed it in fresh from memory.

imageI started writing code on paper. This code wouldn’t run on any real machine, only the one in my head. My parents saw this and decided that it was time I got a chance to save my programs. They bought me an Apple II+ with a floppy drive.

I wrote more and more complex programs. I started looking at the machine code and comparing it to the assembly listing in the back of the Apple II users manual. I still remember the comments in that code with the author’s name: Steve Wozniak. I could see the actual loop that made the cursor blink.

Eventually, my Dad got me a subscription to Byte magazine. He also got me the Apple Macro Assembler and a second floppy drive. You had to put the assembler disk in one and write your program on the other.

After that first machine, I went on to an Apple IIe, various “IBM Clones”, and finally became a professional Windows programmer. I came back to Apple when I discovered that a MacBook Pro was the best available laptop for running Vista. I still use that 2007 MBP today (running Windows 7, and with a hard drive and memory upgrade) as my primary machine.

Steve Jobs and Steve Wozniak (and my Dad, of course) fed my love for software and helped start this adventure. And now one of them is gone. Let us continue the adventure in his honor.

Thought Cloud demo launch

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

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

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.

Thought Cloud UI #5: Arrange your thoughts

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

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

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

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

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.