Skip to main content Skip to footer

The Making of WorkSpace Part 2: Opening and Saving Files

In this blog post series i'm discussing the making of the ComponentOne WorkSpace app for Windows 8. I'm focusing 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 the previous post I talked about the ViewModel structure and commanding used in the app. In this second post I will cover how the app handles opening, saving and detecting changes to files. This topic is useful because it covers reading and writing text files in WinRT, while supporting different encoding types for several different file types. It really is the heart of WorkSpace as it encompasses the key functionality of the app. First I will cover opening and saving text documents. I've included a sample you can download which demonstrates the topics discussed for reading and writing to files using the C1RichTextBox control. It’s a stripped down version of the actual code in WorkSpace that shows the fundamentals that you could use in your own apps. Download Sample: RichTextBoxSave At the end of this post I will address using the GCSpreadSheet control.

Reading Text Files

First, let’s look at how we can read text files and set content to the C1RichTextBox. The C1RichTextBox control works like a TextBox; it has a Text property as well as an Html property and you can set these properties to content read from any file. When setting HTML content to C1RichTextBox, you use the SetHtml method or the Html property. The benefit of using SetHtml() is that you can specify a custom base URI (if you know one exists). In this case we will let the control determine the base URI given the string of html, so we pass in null to the SetHtml method. MakingWorkSpace2_1 When setting RTF content to C1RichTextBox, we must use the separate RtfFilter class. This class has a few functions, such as ConvertToDocument, that allow us to convert RTF to a Document that C1RichTextBox understands. The following code snippet displays a FileOpenPicker to the user, reads a file they select, and sets content to a C1RichTextBox control depending on whether it’s HTML or RTF.


// open file picker  
FileOpenPicker openPicker = new FileOpenPicker();  
openPicker.FileTypeFilter.Add(".html");  
openPicker.FileTypeFilter.Add(".htm");  
openPicker.FileTypeFilter.Add(".rtf");  
StorageFile file = await openPicker.PickSingleFileAsync();  
if (file != null)  
{  
    if (file.FileType.Equals(".html") || file.FileType.Equals(".htm"))  
    {  
        // Open HTML and set to C1RichTextBox  
        try  
        {  
            c1RichTextBox1.SetHtml(await ReadFileText(file), null);  
        }  
        catch  
        {  
            c1RichTextBox1.Text = "Error loading HTML file.";  
            return;  
        }  
    }  
    else if (file.FileType.Equals(".rtf"))  
    {  
        // Open RTF and set to C1RichTextBox  
        try  
        {  
            c1RichTextBox1.Document = new RtfFilter().ConvertToDocument(await ReadFileText(file));  
        }  
        catch  
        {  
            c1RichTextBox1.Text = "Error loading RTF file.";  
            return;  
        }  
    }  
}  

In the code above I am also referring to a method named ReadFileText. It handles reading text files while taking different encoding types into account. The FileIO.ReadTextAsync approach might be the first thing you try when reading text files, but it has a limited encoding options available and in a text editor type of app like WorkSpace, you need to be able to detect the encoding and open appropriately. The ReadFileText method reads the file as bytes instead so it can more easily determine the encoding used. It’s a useful method if you need to read an assortment of text files in WinRT.


/// <summary>  
/// Handles reading file contents considering encoding types and returns it as a string  
/// </summary>  
/// <returns></returns>  
private async Task<string> ReadFileText(StorageFile file)  
{  
    string text = null;  
    if (file != null)  
    {  
        // read content into buffer  
        var buffer = await Windows.Storage.FileIO.ReadBufferAsync(file);  
        using (DataReader dataReader = DataReader.FromBuffer(buffer))  
        {  
            // get bytes from buffer  
            var data = new byte[buffer.Length];  
            dataReader.ReadBytes(data);  
            // convert bytes into string  

            if (data.Length > 1 && data[0] == 0xff && data[1] == 0xfe) // little-endian Unicode  
            {  
                text = System.Text.Encoding.Unicode.GetString(data, 0, data.Length);  
            }  
            else if (data.Length > 1 && data[0] == 0xfe && data[1] == 0xff) // big-endian Unicode  
            {  
                text = System.Text.Encoding.BigEndianUnicode.GetString(data, 0, data.Length);  
            }  
            else  
            {  
                try  
                {  
                    // should work for most non-unicode files (ASCII, 1252, etc - but not UTF7)  
                    text = System.Text.Encoding.UTF8.GetString(data, 0, data.Length);  
                }  
                catch  
                {  
                    var encoding = System.Text.Encoding.GetEncoding("Windows-1252");  
                    text = encoding.GetString(data, 0, data.Length);  
                }  
            }  
        }  
    }  
    return text;  
}  

Writing Text Files

When an HTML or RTF file is being saved we obtain the content from the C1RichTextBox and write it to a file using FileIO.WriteTextAsync. When getting HTML we can either use the GetHtml method or simply the Html property. The benefit of using GetHtml() is that we can decide how we want styles to be incorporated into the document: inline or as a style sheet. By default the WorkSpace app makes all styles into style sheets, but this could very well become a setting in the future.


FileSavePicker savePicker = new FileSavePicker();  
savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;  
savePicker.FileTypeChoices.Add("HTML Page (*.html)", new List<string>() { ".html", ".htm" });  
savePicker.FileTypeChoices.Add("Rich Text Format (*.rtf)", new List<string>() { ".rtf" });  
savePicker.DefaultFileExtension = ".html";  
StorageFile file = await savePicker.PickSaveFileAsync();  
if (file != null)  
{  
    if (file.FileType.Equals(".html") || file.FileType.Equals(".htm"))  
    {  
        // Save HTML  
        await FileIO.WriteTextAsync(file, c1RichTextBox1.GetHtml(HtmlEncoding.StyleSheet));  
    }  
    else if (file.FileType.Equals(".rtf"))  
    {  
        // Save RTF  
        await FileIO.WriteTextAsync(file, new RtfFilter().ConvertFromDocument(c1RichTextBox1.Document));  
    }  
}  

Detecting Changes

A good text editor only needs to save a file when there are changes made to the document. In WorkSpace I track these changes by flagging a Boolean property on the DocumentViewModel. Then, the saving logic only runs if the file is flagged. Also, an indicator is shown to the user once unsaved changes have been made. MakingWorkSpace2_2 The key thing to tracking changes is being able to efficiently detect when changes occur. One way to do this with C1RichTextBox is to subscribe to the DocumentHistory.HistoryChanged event. The C1RichTextBox control supports document history (undo/redo) and this is the simplest and most efficient approach to detect changes.


// rtb is our C1RichTextBox control  
rtb.DocumentHistory.HistoryChanged += DocumentHistory_HistoryChanged;  
void DocumentHistory_HistoryChanged(object sender, EventArgs e)  
{  
    this.Document.HasChanges = true;  
}  

I’m tracking changes in the code-behind of the UserControl containing C1RichTextBox. An instance of the DocumentViewModel is accessed through a Document property which is set when the editor is initialized. Once the save operation is completed successfully, the HasChanges property is set to false.

Working with Spreadsheets

So far I’ve only talked about reading and writing text files to be used with C1RichTextBox. On the other side of WorkSpace is the spread sheet functionality which uses the GCSpreadSheet control (part of ComponentOne Spread WinRT). The GCSpreadSheet control has an OpenExcelAsync method that I use to open a file directly. The method accepts a stream, so I make a stream out of the StorageFile using its OpenStreamForReadAsync method. I also added support for CSV files by using a separate OpenCsvAsync method. Since a CSV file contains just one sheet, this method must be set on a specific sheet, such as through the ActiveSheet property. The following code shows how to load an Excel file using the GCSpreadSheet control.


if (file.FileType.Equals(".xls") || file.FileType.Equals(".xlsx"))  
{  
    var stream = await file.OpenStreamForReadAsync();  
    await this.gcSpreadSheet1.OpenExcelAsync(stream);  
    stream.Dispose();  
    foreach (var sheet in this.gcSpreadSheet1.Sheets)  
    {  
        if (sheet.RowCount < Int16.MaxValue) sheet.RowCount = Int16.MaxValue;  
        if (sheet.ColumnCount < 256) sheet.ColumnCount = 256;  
    }  
}  
else if (file.FileType.Equals(".csv"))  
{  
    var stream = await file.OpenStreamForReadAsync();  
    await this.gcSpreadSheet1.ActiveSheet.OpenCsvAsync(stream, TextFileOpenFlags.None);  
    stream.Dispose();  
}  

When saving a spreadsheet, I took advantage of the various save methods on the GCSpreadSheet control, such as SaveExcelAsync, SaveCSVAsync, SaveXmlAsync and SavePdfAsync, to give the user plenty of options. And just like when the file was opened, I needed to first obtain a stream from the storage file and pass that to the control. The following code shows how to save an Excel file using the GCSpreadSheet control.


var ras = await file.OpenAsync(FileAccessMode.ReadWrite);  
ras.Size = 0;  
using (var stream = ras.AsStreamForWrite())  
{  
    var fileName = file.FileType.ToUpperInvariant();  

    if (fileName.EndsWith(".XLSX") || fileName.EndsWith(".XLS"))  
    {  
        var fileFormat = ExcelFileFormat.XLS;  
        if (fileName.EndsWith(".XLSX"))  
            fileFormat = ExcelFileFormat.XLSX;  

        await this.gcSpreadSheet1.SaveExcelAsync(stream, fileFormat);  
    }  
    else if (fileName.EndsWith(".CSV"))  
    {  
        await this.gcSpreadSheet1.SaveCSVAsync(gcSpreadSheet1.ActiveSheetIndex, stream, TextFileSaveFlags.AsViewed);  
    }  
    else if (fileName.EndsWith(".XML"))  
    {  
        await this.gcSpreadSheet1.SaveXmlAsync(stream);  
    }  
    else if (fileName.EndsWith(".PDF"))  
    {  
        int[] sheets;  
        sheets = new int[gcSpreadSheet1.SheetCount];  
        for (int i = 0; i < gcSpreadSheet1.SheetCount; i++)  
        {  
            sheets[i] = i;  
        }  
        await this.gcSpreadSheet1.SavePdfAsync(stream, sheets);  
    }  

    stream.Dispose();  
}  

Detecting changes in the GCSpreadSheet control is a bit more complex than with C1RichTextBox, because there is more than one single event that should be listened to. The ActiveSheet has four events I listen for changes in: PropertyChanged, CellChanged, RowChanged, and ColumnChanged. These events need wired up for each sheet that is activated, so in WorkSpace I have them set in the ActiveSheetChanged event.


void gcSpreadSheet1_ActiveSheetChanged(object sender, EventArgs e)  
{  
    this.gcSpreadSheet1.ActiveSheet.PropertyChanged += ActiveSheet_PropertyChanged;  
    this.gcSpreadSheet1.ActiveSheet.CellChanged += ActiveSheet_CellChanged;  
    this.gcSpreadSheet1.ActiveSheet.RowChanged += ActiveSheet_RowColumnChanged;  
    this.gcSpreadSheet1.ActiveSheet.ColumnChanged += ActiveSheet_RowColumnChanged;  
}  

Conclusion

This post covered an important part of the WorkSpace app, the actual file management. Next, I will go deeper into usage of these controls as I show how to use the new C1RadialMenu control to provide contextual menus for editing files.

comments powered by Disqus