Skip to main content Skip to footer

The Making of WorkSpace Part 1: The ViewModel

A few weeks ago we released the ComponentOne WorkSpace app and it’s been quite a success with over 100 downloads per day and great user feedback. It’s a first generation spreadsheet and rich text editor all-in-one. Oh, and it also opens PDF files too just for fun. In this blog post series I will talk about making the initial version of this app and focus on four key areas that may help you build your own Windows Store apps.

  1. App View and ViewModel structure
  2. Opening and saving Files
  3. Using the C1RadialMenu control to format content
  4. Recent documents list

In this first post I will focus on the View and ViewModel structure of the application, as well as commanding and tiles. WorkSpace is fundamentally a text editor with specialized editor controls for different file types. Even though a text editor isn’t normally the best example of an application you would write with MVVM, it’s really become the only way I write any applications. I don’t write the strictest MVVM code; I try to do whatever makes the most sense for each part of the application. I follow MVVM primarily to help write reusable code, as well as for the data binding and item selection benefits on the UI. I started the application by selecting the GridView template. I chose this template because I appreciate a lot of the extra classes and styles that Microsoft put into this template such as LayoutAwarePage and BooleanToVisibilityConverter to name a few. The first thing I did was remove the pages and add my own. The WorkSpace app has just two pages: MainPage and EditorPage. The MainPage displays the initial commands and a list of recent documents. The EditorPage displays a selected file opened in an editor with additional commands for editing the file. I’ll get into more of the UI layout later, for now it’s just good to understand the basic UI structure. WorkSpace_part1_1 Now onto the ViewModels. There’s a MainViewModel which is the data context for both pages, and a DocumentViewModel which holds the business logic for one individual document. The MainViewModel is responsible for several app-wide areas of logic including document commands, recent and selected document logic, as well as file sharing. The complete ViewModel diagrams looks like this: WorkSpace_part1_2

Commands

The MainViewModel also has several commands (not shown) that initiate some common action, including:

  • NewDocumentCommand
  • SaveDocumentCommand
  • OpenDocumentCommand

The commands are pretty self-explanatory by name. The New and Open commands perform logic in loading or creating a document view model as well as navigating to the editor page. The save command kicks off the saving code for the open document. The commands initiate the action, but the actual logic for loading and saving documents rests in the DocumentViewModel. This structure allows for fewer and more resusable methods. The commanding is handled using the C1.Xaml.C1Command and C1.Xaml.CommandManager classes. For instance, here is the initialization of the SaveDocumentCommand in code:


using C1.Xaml;  

public static readonly C1Command SaveDocumentCommand = new C1Command("Save", typeof(MainViewModel));  

static MainViewModel()  
{  
    // register commands  
    CommandManager.RegisterClassCommandBinding(typeof(MainViewModel), new CommandBinding(SaveDocumentCommand, SaveDocument, CanSaveDocument));  
}  

private static async void SaveDocument(object sender, ExecutedRoutedEventArgs e)  
{  
    // get command target from the sender's DataContext  
    FrameworkElement btn = sender as FrameworkElement;  
    if (btn != null)  
    {  
        //get view model and perform logic  
    }  
}  

/// <summary>  
/// Determines if command can be executed, if false command button will appear disabled  
/// Note: to improve performance do not use CanExecute methods where not needed  
/// </summary>  
private static void CanSaveDocument(object sender, CanExecuteRoutedEventArgs e)  
{  
    e.CanExecute = true;  
}  

C1Command is handy because the WinRT platform, like Silverlight, does not include a full implementation of ICommand. Basically, you would use C1Command instead of a custom RelayCommand or DelegateCommand. Later in this post I show how to invalidate commands to update their CanExecute status. The figure below shows the command usage on the MainPage. A line indicates a binding or a static property is being set. WorkSpace_part1_3 DataContext and CommandParameters are used to provide conditional results based upon the source (ie, determine if we are opening a document from the recent document list or from an open file picker). On the view side, when using static commands then you directly set the command rather than use a Binding, like the following:


<Button x:Name="btnSave" c1:CommandExtensions.Command="data:ViewModel.SaveDocumentCommand" />  

Tiles

The C1Tile control is used for all tiles on the MainPage. What makes C1Tile better than a simple Border element? In addition to sliding and flipping animations which are not being used here, it has built-in support for pointer down animation so the tile responds to the users touch like a tile on the Windows Start screen. It’s a small touch but it makes your app feel a bit more alive. You need to only set one attached property to enable this behavior:


<c1tile:C1Tile c1tile:C1TileService.PointerDownAnimation="True" ...  

Document Selection

On the left side of the MainPage, you’ll notice the app maintains and displays a list of recent documents opened by the user. This list is managed by the MainViewModel. I’ll discuss how the recent documents are stored in a later post, but for now I’ll just mention that the MainViewModel handles this logic and exposes it to the view (bound to an ItemsControl). The WorkSpace app also maintains and displays a list of open documents displayed on the EditorPage. Users can toggle between these documents as if they had multiple files open on their desktop, but there’s only one instance of WorkSpace running. The figure below shows how the open documents are displayed using the Top AppBar, as well as the commands in the Bottom AppBar on the EditorPage. A line indicates a binding or property being set. WorkSpace_part1_4 In a future update we will add document thumbnails to the top app bar. The Open File and New File buttons here are set to the same commands as the tiles on the home page. There’s a new command, CloseDocumentCommand, which is actually on the DocumentViewModel. OpenDocuments is a collection of DocumentViewModels, each with info concerning an individual document. And SelectedDocument is a single DocumentViewModel that can be set by the UI. One of my favorite aspects of using MVVM is selected item logic. Anytime you have a list of items you can use a TwoWay binding to an instance of the list item and your entire selection logic is completed. Any other UI elements bound to my SelectedDocument property from the ViewModel will be automatically updated. There’s no need for SelectionChanged events or anything like that following this basic MVVM scenario. For example, here’s what the XAML looks like for the open documents ListBox:


<ListBox ItemsSource="{Binding OpenDocuments}" SelectedItem="{Binding SelectedDocument, Mode=TwoWay}" ...  

And here are the OpenDocuments and SelectedDocument properties defined in the MainViewModel:


private ObservableCollection<DocumentViewModel> _openDocuments = new ObservableCollection<DocumentViewModel>();  
public ObservableCollection<DocumentViewModel> OpenDocuments  
{  
    get { return this._openDocuments; }  
}  

private DocumentViewModel _selectedDocument;  
public DocumentViewModel SelectedDocument  
{  
    get  
    {  
        return _selectedDocument;  
    }  
    set  
    {  
        _selectedDocument = value;  
        OnPropertyChanged("SelectedDocument");  
        CommandManager.InvalidateRequerySuggested();  
    }  
}  

This is a very basic item selection model that can be used in almost any XAML application where selection is required. You’ll notice I’m also calling CommandManager.InvalidateRequerySuggested after each time the SelectedDocument changes. What this does is update my commands CanExecute status. For instance, WorkSpace allows the user to save all document types except for PDF. So when a user opens a PDF file the Save button becomes disabled. Back in the days of WPF commanding, we didn’t have to call any methods on the CommandManager to invalidate our commands, but since Silverlight and WinRT, we must explicitly tell our app when to check if commands can execute. Here’s an updated CanSaveDocument method that disables the save command when the user opens a PDF file.


/// <summary>  
/// Determines if command can be executed, if false command button will appear disabled  
/// Note: to improve performance do not use CanExecute methods where not needed  
/// </summary>  
private static void CanSaveDocument(object sender, CanExecuteRoutedEventArgs e)  
{  
    FrameworkElement btn = sender as FrameworkElement;  
    if (btn != null)  
    {  
        ViewModel viewModel = btn.DataContext as ViewModel;  
        if (viewModel != null)  
        {  
            if (viewModel.SelectedDocument != null)  
            {  
                if (viewModel.SelectedDocument.DocumentType == DocumentTypeEnum.PDF)  
                {  
                    if (viewModel.SelectedDocument.EditorType == EditorTypeEnum.PdfViewer)  
                    {  
                        e.CanExecute = false;  
                    }  
                    else  
                    {  
                        e.CanExecute = true;  
                    }  
                }  
                else  
                {  
                    e.CanExecute = true;  
                }  
            }  
        }  
    }  
}  

I have to distinguish between DocumentType and EditorType because it’s possible to save a spreadsheet as PDF yet still edit it using the spreadsheet editor. So here I only disable the command when both properties are PDF in nature. When the app bars are closed, I added another save button to the top right corner of the EditorPage. This is purely for user convenience as it’s the most commonly used button in the app. WorkSpace_part1_5

Editor Type

The editor type (ie, RichTextBox, Spreadsheet, PdfViewer) is determined when a document is opened, and an instance of an editor UserControl is set to the Control property on the DocumentViewModel. This is one part of the application that breaks ideal MVVM practice. But in the case of WorkSpace, it makes communication back and forth between the ViewModel and the editor control easier to manage, than say using a DataTemplateSelector. The following diagram shows the various editor control types, as well as which file formats they support. WorkSpace_part1_6 To bind a UI element to a UserControl, you use a ContentControl element. Here we bind to the SelectedDocument.Control property.


<ContentControl Content="{Binding SelectedDocument.Control}" HorizontalContentAlignment="Stretch"/>  

In the next post I discuss techniques for reading and writing to files from the WorkSpace app. You can download the WorkSpace app on your Windows 8 device following links from my previous post here.

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus