Silverlight IsolatedStorage Compressed Object Cache

One of the most useful features of Silverlight 3 is the IsolatedStorage mechanism, which gives the developer access to the filesystem on the client to store data. Now that Silverlight has matured sufficiently to be used in line – of – business and Enterprise – level applications, having a pre-built facility to serialize, compress, store and load multiple objects in IsolatedStorage can be helpful.

This SilverlightCache class can use either the DataContractSerializer to serialize your objects for compression and storage, or it can use the much faster, more compact Custom Binary Serializer that I featured in a previous article.

The DataContractSerializer does not require any pre-setup or interface implementation on your classes. While the Custom Binary Serializer I present here requires you to implement a simple interface on your classes, the extra work may well be worth it. It is twice as fast, and serializes your objects to about one –half the size that the DataContractSerializer does. In any case, there's code to do both, so you decide.

In either case, I use a Silverlight port of MiniLzo by Markus Oberhumer that was put together by Owen Emlen in C#. In general you can expect anywhere from 50% to 75% compression with this. It is also reasonably fast.

Here is an example of how to implement the ICustomBinarySerializable interface in a class:

[Serializable]
public class NotesList: ICustomBinarySerializable
{
    public List<Note> Notes = new List<Note>();
    #region ICustomBinarySerializable Members
     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);
              // You can also just do this:             

                            // c.WriteDataTo(_Writer);


            }
        }

        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();
                //You can also just do this:                 

                              // c.SetDataFrom(_Reader );


                 this.Notes.Add(c);
            }
        }
    }

    #endregion
}

Note above that I use a while loop to handle reading multiple objects into a List<Note> collection.  In the example code, the Note class that is contained in the List  doesn’t even need to be marked Serializable as its CustomBinarySerializable interface implementation is never called. However, I have it in the code for the sake of completeness.
 

Here is an example of how you would use the Silverlight IsolatedStorage Compressed Object Cache to Store, and then to reload a Collection:

private void butGenerate_Click(object sender, RoutedEventArgs e)
       {
           NotesList list = new NotesList();
           Silverlight.Utils.CacheUtility<NotesList> util = null;
           for(int i=0;i<5000;i++)
           {
               Note note = new Note(DateTime.Now.ToString(), "Subject Line " +i.ToString( ),"This is note " +i.ToString( ),1, "");
               list.Notes.Add(note);
           }
           util = new CacheUtility<NotesList>();
           util.IncreaseQuota();
           util.SaveDataCustom(list);
           // To do this with the DataContractSerializer (SLOWER!) comment line above, and uncomment line below
         //  util.SaveData(list);
           this.Text1.Text = util.UncompressedSize.ToString() + " Uncompressed, " + util.CompressedSize.ToString() +
                             " compressed. Time: " + util.ElapsedMilliseconds.ToString() + " ms.";
       }

       private void butShowData_Click(object sender, RoutedEventArgs e)
       {
           var util = new CacheUtility<NotesList>();
          NotesList list = util.LoadDataCustom();
           this.Text1.Text = util.UncompressedSize.ToString() + " Uncompressed, " + util.CompressedSize.ToString() +
                            " compressed. Time: " + util.ElapsedMilliseconds.ToString() + " ms.";
          // To do this with the DataContractSerializer (SLOWER!) comment line above, and uncomment line below
         //  NotesList list = util.LoadData();
           this.Grid1.ItemsSource = list.Notes;
       }
   }

Notice in the Generate button click handler, I make a call to increase the IsolatedStorage quota. The user will get a Silverlight prompt the first time, and they can OK it. The IncreaseQuota method is in the CacheUtility class.

Then I create 5,000 unique “Note” Instances and add each to the NotesList collection class. FInally I call the SaveDataCustom method of the CacheUtility class.  The CacheUtility provides me with statistics on the sizes and elapsed time to perform the operation.

Now let’s switch over the actual SaveDataCustom method in the CacheUtility class and see how it works:

public void SaveDataCustom(T data)
        {
            DateTime start = DateTime.Now;
             using (IsolatedStorageFile isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())
            {
                DeleteData();

                 using (IsolatedStorageFileStream fileStream = isolatedStorageFile.CreateFile(CacheFilename))
                {
                    CustomBinarySerializer.CustomBinaryFormatter f = new CustomBinaryFormatter();
                    f.Register<T >(1);
                    MemoryStream ms = new MemoryStream();
                    f.Serialize(ms, data);
                    ms.Seek(0, 0);
                     byte[] b = ms.ToArray();
                    ms.Dispose();
                    UncompressedSize = b.Length;
                     byte[] compressed = b.Compress();
                    CompressedSize = compressed.Length;
                    fileStream.Write(compressed, 0, compressed.Length);
                 }
            }
            DateTime end = DateTime.Now;
            TimeSpan elapsed = end - start;
             this.ElapsedMilliseconds = elapsed.TotalMilliseconds;
         }

Above, you can see that we first get our IsolatedStorage, then we delete any previous data, then we create our file. Then we create our CustomBinaryFormatter and register the type with it. We create a MemoryStream, serialize the object graph into it, rewind the stream.

Then we get the byte array out of the stream and compress it. Finally, write it to the file. This entire operation may take 250 milliseconds or so with 5,000 of these objects stored. The original 492,788 bytes of serialized object graph is compressed down to only 227,928 bytes – a savings of more than 53 percent. So even without requesting to increase the quota, we’ve got plenty of storage space. You can specify different file names in order to store multiple objects or collections of objects.

You do not "have to" use this concept only for IsolatedStorage. You can serialize and compress objects to send over the wire, for example. Your WCF Service at the other end would have the same code to decompress and deserialize your "stuff" and do what you need to do - update a database, and so on.

The whole concept of this is to create a utility that can easily be “plugged in” to any number of applications to solve a common problem. You can download the complete Visual Studio 2008 SIlverlight 3 project.

By Peter Bromberg   Popularity  (7817 Views)