One of the nice things about being in the software business for a long time is that
you can build up a great library of reusable code and projects to draw on. I
was looking at the new Silverlight 3 OOB ("out of browser") feature,
and I thought - "this would be great for a small desktop Note-taker application.
I could use Isolated Storage to store the data, and it would work great offline."
So, I built one. I decided to use my Custom Binary Serialization classes to handle storing the local "database" in compact byte array form.
With Isolated Storage, you really cannot create a relational database unless
you're ready to write a SQL Parser and all kinds of file-seeking code. But what
you can do is store a generic Dictionary or List or containing your objects,
apply binary serialization to it, and simply load and save the entire collection
as a compact byte array. This will work great for small amounts of data - say
up to a few thousand records, plus you have the advantage of dealing with objects
instead of SQL. Throw in a little LINQ and some lambdas, and you have everything
you need to load, save, search and so on.
I start out with the objects - we have a Note class, and a NoteList class to hold
all our "Notes":
Note.cs class:
using System;
using System.IO;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using CustomBinarySerializer;
namespace SerializerTest
{
[Serializable]
public class Note : ICustomBinarySerializable
{
public Note()
{
}
public Note(string entryDate, string subject, string noteText, int priority, string reminder)
{
this.ID = Guid.NewGuid().ToString();
this.EntryDate = entryDate;
this.Subject = subject;
this.NoteText = noteText;
this.Priority = priority;
this.Reminder = reminder;
}
public string ID { get; set; }
public string EntryDate { get; set; }
public string Subject { get; set; }
public string NoteText { get; set; }
public int Priority { get; set; }
public string Reminder { get; set;}
public void WriteDataTo(BinaryWriter _Writer)
{
_Writer.Write((string)ID);
_Writer.Write((string) EntryDate);
_Writer.Write((string) Subject);
_Writer.Write((string) NoteText);
_Writer.Write((int)Priority);
_Writer.Write((string)Reminder);
}
public void SetDataFrom(BinaryReader _Reader)
{
ID
= _Reader.ReadString();
EntryDate
= _Reader.ReadString();
Subject = _Reader.ReadString();
NoteText
= _Reader.ReadString();
Priority
= _Reader.ReadInt32();
Reminder
= _Reader.ReadString();
}
}
}
NoteList.cs Class:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using CustomBinarySerializer;
namespace SerializerTest
{
[Serializable]
public class NoteList: ICustomBinarySerializable
{
public List<Note> Notes;
public NoteList()
{
Notes = new List<Note>();
}
public void WriteDataTo(BinaryWriter _Writer)
{
foreach(Note c in this.Notes )
{
_Writer.Write(c.ID);
_Writer.Write(c.EntryDate);
_Writer.Write(c.Subject);
_Writer.Write(c.NoteText);
_Writer.Write(c.Priority
);
_Writer.Write(c.Reminder);
}
}
public void SetDataFrom(BinaryReader _Reader)
{
while (_Reader.BaseStream.Position < _Reader.BaseStream.Length )
{
Note
c = new Note();
c.ID
= _Reader.ReadString();
c.EntryDate
= _Reader.ReadString();
c.Subject
= _Reader.ReadString();
c.NoteText
= _Reader.ReadString();
c.Priority
= _Reader.ReadInt32();
c.Reminder
= _Reader.ReadString();
this.Notes.Add(c);
}
}
}
}
I won't go into details about the BinarySerializer classes here, you can find the
details at the original article linked above, and the project for it is included
in the downloadable solution for this article.
Next, I need some "plumbing" code which I'll put in my Silverlight App
class:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using CustomBinarySerializer;
namespace SerializerTest
{
public partial class App : Application
{
public static byte[] CurrentData;
public static SerializerTest.NoteList Notes;
public static string CurrentItemId;
public Grid root = null;
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
{
System.IO.IsolatedStorage.IsolatedStorageFileStream
fileStream;
if (!store.FileExists("Data.dat"))
{
fileStream
= store.CreateFile("Data.dat");
fileStream.Close();
}
}
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
// this.RootVisual = new Page();
root
= new Grid();
root.Name
= "MainPage";
root.Children.Add(new Page());
this.RootVisual = root;
}
private void Application_Exit(object sender, EventArgs e)
{
WriteDataToStore(CurrentData);
}
public static void SaveNotes()
{
CustomBinarySerializer.CustomBinaryFormatter
f = new CustomBinaryFormatter();
f.Register<NoteList>(1);
MemoryStream
ms = new MemoryStream();
f.Serialize(ms,
Notes);
// rewind to beginning of stream!
ms.Seek(0,
0);
byte[] b = ms.ToArray();
ms.Dispose();
WriteDataToStore(b);
}
public static void WriteDataToStore(byte[] data)
{
if(data==null || data.Length ==0)
return;
using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
{
System.IO.IsolatedStorage.IsolatedStorageFileStream
fileStream = store.CreateFile("Data.dat");
fileStream.Write(data,
0, data.Length);
fileStream.Close();
}
}
public static byte[] ReadDataFromStore()
{
using (var store = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication())
{
System.IO.IsolatedStorage.IsolatedStorageFileStream
fileStream;
fileStream
= store.OpenFile("Data.dat",FileMode.Open);
byte[] b = new byte[fileStream.Length ];
fileStream.Read(b,
0, (int) fileStream.Length);
fileStream.Close();
return b;
}
}
In the constructor, I check to see if my data file exists, and if not, I create one,
"Data.dat". On startup, I've changed the default RootVisual to be a
new Grid. In this manner, I can easily switch pages for simple navigation between
usercontrols. In exit, we want to save any uncommitted changes from the user
session so we write all our data back to Isolated Storage. The "SaveNotes"
method is where, combined with WriteDataToStore, we binary serialize and save
our "database". I also have a few public static fields that make it
easier to get at my data from anywhere in the application.
In the main Page, I have a Grid with an area at the top for a DataGrid, and several
more rows under it to display buttons for Search, View, Create Note, and Delete
Selected Note:

Here is the codebehind for the main Page:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using CustomBinarySerializer;
namespace SerializerTest
{
public partial class Page : UserControl
{
private string itemId = null;
private Popup myPopup = new Popup();
public Page()
{
InitializeComponent();
PopulateNotes();
}
void ShowPopup(string message )
{
Border border = new Border();
border.BorderBrush
= new SolidColorBrush( Colors.Black );
border.BorderThickness = new Thickness( 5.0 );
StackPanel
myStackPanel = new StackPanel();
myStackPanel.Background
= new SolidColorBrush( Colors.LightGray );
TextBlock tb = new TextBlock();
tb.Text = message;
tb.FontFamily
= new FontFamily("Verdana");
tb.FontSize = 48.0;
tb.HorizontalAlignment = HorizontalAlignment.Left;
tb.VerticalAlignment = VerticalAlignment.Center;
tb.Foreground = new SolidColorBrush( Colors.Black );
Button closePopup = new Button();
closePopup.Content
= "Close";
closePopup.Background =
new SolidColorBrush( Colors.Magenta );
closePopup.FontFamily = new FontFamily( "Verdana" );
closePopup.FontSize =
14.0;
closePopup.Width
= 50.0;
closePopup.Click
+= new RoutedEventHandler(ClosePopup_Clicky);
closePopup.Margin = new Thickness( 10 );
myStackPanel.Children.Add(
tb );
myStackPanel.Children.Add(
closePopup );
border.Child
= myStackPanel;
myPopup.Child
= border;
myPopup.VerticalOffset
= 125.0;
myPopup.HorizontalOffset
=125.0;
myPopup.IsOpen
= true;
}
void ClosePopup_Clicky( object sender, RoutedEventArgs e )
{
myPopup.IsOpen = false;
}
private void PopulateNotes()
{
// create a new NoteList collection class
NoteList
notes = new NoteList();
// get our "database" from Isostorage
byte[] b= App.ReadDataFromStore();
if (b.Length > 0)
{
// Register the NotesList type with the CustomBinaryFormatter
CustomBinarySerializer.CustomBinaryFormatter f = new CustomBinaryFormatter();
f.Register<NoteList>(1);
// Create a new stream to store our stuff
MemoryStream ms = new MemoryStream(b);
ms.Seek(0, 0);
// Deserialize it into a new NotesList object
NoteList cl2 = (NoteList) f.Deserialize(ms);
ms.Dispose();
App.Notes = cl2;
// Bind the grid--
Grid1.ItemsSource = cl2.Notes;
// Check to see if any reminders should be fired
CheckReminders();
}
}
private void CheckReminders()
{
List<Note>
notes = App.Notes.Notes;
foreach(Note n in notes)
{
if (!String.IsNullOrEmpty(n.Reminder))
{
var
reminder = DateTime.Parse(n.Reminder);
if (reminder < DateTime.Now)
ShowPopup("Reminder: " + n.Subject);
}
}
}
private void btn1_Click(object sender, RoutedEventArgs e)
{
// delete selected note in grid
string id = ((Note)Grid1.SelectedItem).ID.ToString();
Note
found = App.Notes.Notes.Single(
delegate(Note n)
{
return n.ID.Equals(id);
});
if (found != null)
App.Notes.Notes.Remove(found);
App.SaveNotes();
}
private void btn2_Click(object sender, RoutedEventArgs e)
{
// Add a new Note
App
app = (App)Application.Current;
// Remove the displayed page
app.root.Children.Clear();
// Show the new page
app.root.Children.Add(new AddNote());
}
private void Grid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// capture the selected note ID from Grid
App.CurrentItemId
=((Note)Grid1.SelectedItem).ID.ToString();
}
private void btnSearch_Click(object sender, RoutedEventArgs e)
{
// show notes that match search filter term
string srch = this.Text1.Text;
var
winners = from n in App.Notes.Notes
where ( n.Subject.Contains( srch) || n.NoteText.Contains(srch))
orderby
n.EntryDate descending
select
n;
this.Grid1.ItemsSource = winners;
}
private void btnView_Click(object sender, RoutedEventArgs e)
{
App.CurrentItemId
= ((Note)Grid1.SelectedItem).ID.ToString();
App
app = (App)Application.Current;
// Remove the displayed page
app.root.Children.Clear();
// Show the new page
app.root.Children.Add(new ViewNote());
}
private void Text1_MouseEnter(object sender, MouseEventArgs e)
{
Text1.Text
= "";
}
}
}
Most of the above code should be self-explanatory. When you click View, I grab the
current Selected row in the Grid, remove the main page, and replace it with the
ViewNote usercontrol. In ViewNote, I find the selected Note and bind fields programmatically:
private void DisplaySelectedNote()
{
string id = App.CurrentItemId;
Note
found = App.Notes.Notes.Single(
delegate(Note n)
{
return n.ID.Equals(id);
});
this.txtEntryDate.Text = found.EntryDate.ToString();
this.txtSubject.Text = found.Subject;
this.txtNote.Text = found.NoteText;
this.txtPriority.Text = found.Priority.ToString();
this.Reminder.Text = found.Reminder;
}
I've modified the Manifest for this app so that when it comes up in the browser,
you can right-click on the app, and choose "install" to run it on the
desktop from that point on. Since it only uses local storage, there is no need
to be connected. The default quota for Isolated Storage for Out of Browser apps
is a higher value than for browser based apps. It is currently 25MB.
Finally, I have a method in the Main Page that iterates over all the notes, checking
for a Reminder Field, and which shows a Popup if you have set any Reminders.
Don't forget the Milk!
You can download the full Silverlight 3 Solution here.