Our next step is to save our notes so they’re still around the next time you restart Sidebar or the computer. We’ll need to figure out:
· How to get the data out of the RichTextBox
· How to save and load the data to disk. Saving data to disk is a little tricky because our Gadget is running in the partially trusted Internet Zone because it’s an XBap. You can find out more about what partial trust means on MSDN or Karen Corby’s blog.
· When to save and load the data for “optimal user experience”. This will become more important later on when we start saving notes to a web service since more than one computer may be updating the notes on the web service and we want to minimize problems where data is overwritten or lost.
How to get data out of the RichTextBox.
Silly me, I thought this was going to be easy. I’d be able to write some code like “object notes = richTextBox.Content” or “richTextBox.Content = notes”. RichTextBox supports more advanced save/load scenario than this which means the code is a bit more complex.
You can Save/Load a portion of the RichTextBox contents, so a simple .Save/.Load method isn’t sufficient. Instead, we have to specify the portion of the content we want to work on.
TextRange range = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd
);
You can also Save/Load from a variety of formats. We’ll choose the “most native”, Xaml.
range.Save(stream, DataFormats.Xaml)
Add the necessary glue, and we have:
private void Save()
{
RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;
TextRange range = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd
);
try
{
using (stream)
{
range.Save(stream, DataFormats.Xaml);
}
trace.TraceInformation("Notes Saved");
richTextBox.Background = Brushes.Green; // Signal save
}
catch (Exception ex)
{
trace.TraceInformation(ex.ToString());
richTextBox.Background = Brushes.Red; // Signal error
}
}
Load ends up being very similar:
private void Load()
{
RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;
TextRange range = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd
);
try
{
using (stream)
if (stream.Length > 0)
range.Load(stream, DataFormats.Xaml);
trace.TraceInformation("Notes loaded");
richTextBox.Background = Brushes.AntiqueWhite; // Signal load
}
catch (Exception ex)
{
trace.TraceInformation(ex.ToString());
richTextBox.Background = Brushes.Red; // Signal error
}
}
How to Load and Save from Disk in Partial Trust
Alright – the above code looks good, but there’s that undefined ‘stream’ variable hanging around. How can we create a FileStream to use here that won’t throw a security exception in partial trust? The answer is an API created just for this purpose called IsolatedStorage (see MSDN or an article by Chris Tavares ). Briefly, Isolated Storage is an API that:
· Provides an API that can open a FileStream without triggering an IOPermission exception under partial trust
· Reads & Writes to a folder hidden deep in the user profile directory (on Vista, C:\Users\Username\AppData\Local\IsolatedStorage). It *cannot* be used to read or write any file not stored under here.
· Provides further Isolation to prevent different Applications and such from being able to see or overwrite each other’s data. There’s plenty of detail here we’re going to ignore for the purposes of this article because it’s out of scope (that’s a subtle joke, folks).
I know what you’re thinking now. Hmm, that seems complicated. Do I really want to have to understand this complex new API just to be able to save some data from a Gadget? Let me save you some anxiety. While IsolatedStorage sounds complicated in design (because it is), using it is quite simple. We’re going to use the code:
IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
"Notes", // Name for the stream
FileMode.OpenOrCreate, // Open the file if it exists, create it if it doesn't
FileAccess.ReadWrite, // Allow me to read & write to the file
IsolatedStorageFile.GetUserStoreForApplication() // Isolate by user & application
);
It turns out that using IsolatedStorage is actually a bit simpler than using a “normal” FileStream or FileInfo object. With this API, you don’t need to worry about finding a special directory (how do I get My Documents again?), other people mucking with your data, whether Vista UAC is going to cause your application to fail because you tried to write a file next to the .Exe and the process is no longer running with Administrator permissions, or whether your app has Full Trust or Partial Trust permissions. That’s all been taken care of for you by the API.
When to Save & Load
I found it a little confusing to figure out when I should save and load content. Loading upon startup is obvious, but we don’t want to Save on shutdown or we may lose data.
UI applications are notorious for not guaranteeing that your shutdown code like _Unloaded will be called and the .NET Garbage Collector ‘lazy deallocation’ design means that sometimes your object is simply torn down without notification as the process is exiting. The 'normal' way to approach this in .NET is to use IDisposable, but that won't work here because we don't control the code that's used to load our control and the default WPF behavior doesn't Dispose() content on shutdown. This is made even less deterministic when we have an XBap running in an IE instance running in a Sidebar. What we really need to do is ‘save on finished changing’. When I’m in a situation like this, the way I approach the problem is to play around and see what works. What events are called when? Which event corresponds best to how I *think* the application should behave? You can see now why I change the background color in the Save and Load events – I wanted a very explicit visual cue of when things were happening as I played around. Eventually I settled on saving in _LostKeyboardFocus which fires whenever you click on some other window after editing.
Scaling the Loading
I’ll address a future need now since we’re on the topic of Save & Load. When we start to support “Save/Load from web service” in the future, the contents of my notes may change because I made a change on another machine. One way to detect this would be to hold a network connection (e.g. a socket) open and use some form of notification mechanism (e.g. a callback in WCF terms) to tell me when something has been changed on the server. This is an excellent design but it doesn’t scale very well. Since my intention is to host a “notes web service” on my home computer over my waaay too slow DSL line and then publish it to this blog, I really didn’t want to design a “detect change” mechanism that would cause my server (which, by definition, is one of my old desktops for a cheapskate like me) to hold network connections open to potentially thousands of blog readers. I also didn’t want to use a ‘polling’ design that would cause thousands of computers, which may be idle, to poll my server on a regular basis. I decided to go with a “load when the user is interested” approach.
I defined “user is interested” as “user mouses over the notes form”. With some more experimentation, I settled on an algorithm of when Page_MouseEnter fires and !IsKeyboardFocusWithin. This wasn’t very intuitive and the eventing interaction of an XBap within a Gadget within a Sidebar was a bit strange so I followed the "how I *think* the application should behave" approach mentioned above to figure this out. Once I decided on this algorithm, it pretty much makes sense when put into words. “If you mouse over the form and you’re not already in the process of editing the notes, reload.” When I left off the !IsKeyboardFocusWithin, I kept hitting the ‘less than useful’ behavior that I would be in the middle of editing the notes with the keyboard, I’d jiggle the mouse off and back onto the form, and this would fire Page_MouseExit/Page_MouseEnter which would load the last saved copy of my notes losing all the changes I was making.
What does the code look like now?
using System;
using System.Diagnostics;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace NetNotes
{
public partial class Page1 : Page
{
TraceSource trace = new TraceSource("NetNotes", SourceLevels.Information);
public Page1()
{
InitializeComponent();
trace.TraceInformation("Initialized");
Load();
MouseEnter += Page1_MouseEnter;
LostKeyboardFocus += Page1_LostKeyboardFocus;
}
void Page1_MouseEnter(object sender, MouseEventArgs e)
{
if (!IsKeyboardFocusWithin)
Load();
}
void Page1_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Save();
}
private void Save()
{
RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;
TextRange range = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd
);
try
{
using (
IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
"Notes",
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
IsolatedStorageFile.GetUserStoreForApplication()
))
range.Save(stream, DataFormats.Xaml);
trace.TraceInformation("Notes Saved");
richTextBox.Background = Brushes.Green; // Signal save
}
catch (Exception ex)
{
trace.TraceInformation(ex.ToString());
richTextBox.Background = Brushes.Red; // Signal error
}
}
private void Load()
{
RichTextBox richTextBox = this.FindName("NotesBox") as RichTextBox;
TextRange range = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd
);
try
{
using (
IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
"Notes",
FileMode.OpenOrCreate,
FileAccess.Read,
IsolatedStorageFile.GetUserStoreForApplication()
))
if (stream.Length > 0)
range.Load(stream, DataFormats.Xaml);
trace.TraceInformation("Notes loaded");
richTextBox.Background = Brushes.AntiqueWhite; // Signal load to user
}
catch (Exception ex)
{
trace.TraceInformation(ex.ToString());
richTextBox.Background = Brushes.Red; // Signal error
}
}
}
}