Silverlight 3 Note-Taker App With Local Storage

A Silverlight 3 "Out of Browser" capable Note-taking app that uses Isolated Storage for it's "database". Binary Serialization is used.

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.

By Peter Bromberg   Popularity  (3140 Views)