Thought Cloud UI #4: Open the clouds

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.

Leave a Reply

You must be logged in to post a comment.