Silverlight 2 Beta 2: Doing Data Part III

For this exercise we put aside our Quotations database work: We'll use the DataContractSerializer to serialize a Generic List of type Note. The serialized List will then be compressed with SharpZipLib, and finally we'll save the compressed byte array in an IsolatedStorage File so that we can load, decompress, deserialize, and get our Notes List back on demand.

Silverlight: Compressing and Serializing Objects to Isolated Storage

Rob Houweling
did a pretty decent job of porting the ICSharpCode.SharpZipLib library to Silverlight. I wonder how many people realize what a tremendous favor he did for us.

Now you can extract and decompress resources from your SilverLight xap file on demand; you can compress objects to compact byte arrays for sending over the wire, and a host of other useful things

For this exercise I've put aside my Quotations database work in favor of a different take on working with data: We'll use the DataContractSerializer to serialize a Generic List of type Note (a small class representing a "note" with item, description, dueDate, status, etc.). The serialized List will then be compressed with SharpZipLib, and finally we'll save the compressed byte array in an IsolatedStorage File so that we can load, decompress, deserialize, and get our Notes List back on demand, such as when you first load the app.

We'll display the Notes in a Silverlight DataGrid and add the options to create a new Note and save our work in Isolated Storage.

The easiest way to get started with this one is just to download the solution zip file from the bottom of this article, and load it into Visual Studio.

First, you need to create a class that represents the Data-bindable object you want to work with. In this case, its "Note":

using System;
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 System.Runtime.Serialization;

namespace NotesSerializer
{
    public enum Priority
    {
        Low,
        Medium,
        High
    }

    public enum Category
    {
        Personal,
        Home,
        Business
    }
   
    [DataContract]
    public class Note
    {
        [DataMember]
        public string Item { get; set; }
        [DataMember]
        public string Description { get; set; }
        [DataMember]
        public Priority Priority { get; set; }
        [DataMember]
        public Category Category { get; set; }
        [DataMember]
        public DateTime DueDate { get; set; }
        [DataMember]
        public bool Status {get;set;}
        
        public Note()
        {
        }

        public Note(string item, string description, Priority priority, Category category, DateTime dueDate, bool status )
        {
            this.Item = item;
            this.Description = description;
            this.Priority = priority;
            this.Category = category;
            this.DueDate = dueDate;
            this.Status = status;
        }
    }
}

You can see that I have added the required [DataContract] and [DataMember] attributes that the DataContractSerializer needs in order to know what to do with the instance you feed it. There are other attributes that you can look up in the documentation, but we don't need any of them here.

I was going to use the XmlSerializer, but after reading the Silverlight documentation on it I realized it probably was not going to buy me anything special. I even thought about porting Angelo Scotto's CompactFormatter to get binary serialization but I quickly realized it was just too much work. I see that Rocky Lhotka has already started some similar work in this vein for his CSLA; Rocky, I wish you luck! The bottom line here is that you've got some 4.6MB of stuff to install for the user to get Silverlight, and there's only so much "cool stuff" you can pack into it. Painful decisions have to be made, so get over it. It's natural that the average developer won't agree with what decisions were made -- all you need to do is look at the ridiculous posts on the Silverlight Forums about a petition to bring back synchronous WebRequests. DOH! (The moderators deleted the entire thread as it had sunk into an ad-hominem hatefest).

Now that I have my Note class, I need a wrapper over the SharpZipLib library in order to make compressing / decompressing and Serialization easier to work with. This was easy to add since I already had one from some previous work. It only needed minor additions for the Serialization:

using System;
using System.Text;
using System.IO;
using System.Collections;
using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace ICSharpCode.SharpZipLib
{
    public class Wrapper
    {
        public Wrapper()
        {
        }
        public  byte[] Serialize (Object inst)
        {
            Type t = inst.GetType();
            DataContractSerializer dcs = new DataContractSerializer(t);
            MemoryStream ms = new MemoryStream();
            dcs.WriteObject(ms, inst);
            return ms.ToArray();
        }

        public  Object Deserialize (Type t, byte[] objectData)
        {
            DataContractSerializer dcs = new DataContractSerializer(t);
            MemoryStream ms = new MemoryStream(objectData);
          return  dcs.ReadObject(ms);
        }

        public byte[] SerializeAndCompress(Object inst)
        {
            byte[] b = Serialize(inst);
            byte[] b2 = Compress(b);
            return b2;
        }

        public Object DecompressAndDeserialize(Type t, byte[] bytData)
        {
            byte[] b = Decompress(bytData);
            Object o = Deserialize(t, b);
            return o;
        }

        public byte[] Compress(string strInput)
        {            
            try
            {
                byte[] bytData = System.Text.Encoding.UTF8.GetBytes(strInput);         
                MemoryStream ms = new MemoryStream();
                ICSharpCode.SharpZipLib.Zip.Compression.Deflater  defl =
new ICSharpCode.SharpZipLib.Zip.Compression.Deflater(9,false); Stream s =
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.DeflaterOutputStream(ms,defl); s.Write(bytData, 0, bytData.Length); s.Close(); byte[] compressedData = (byte[])ms.ToArray(); return compressedData; } catch { throw; } } public byte[] Compress(byte[] bytData) { try { MemoryStream ms = new MemoryStream(); ICSharpCode.SharpZipLib.Zip.Compression.Deflater defl =
new ICSharpCode.SharpZipLib.Zip.Compression.Deflater(9, false); Stream s =
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.DeflaterOutputStream(ms, defl); s.Write(bytData, 0, bytData.Length); s.Close(); byte[] compressedData = (byte[])ms.ToArray(); return compressedData; } catch { throw; } } public byte[] Compress(byte[] bytData, params int[] ratio) { int compRatio=9; try { if ( ratio[0] >0 ) { compRatio=ratio[0]; } } catch { } try { MemoryStream ms = new MemoryStream(); ICSharpCode.SharpZipLib.Zip.Compression.Deflater defl =
new ICSharpCode.SharpZipLib.Zip.Compression.Deflater(compRatio,false); Stream s =
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.DeflaterOutputStream(ms,defl); s.Write(bytData, 0, bytData.Length); s.Close(); byte[] compressedData = (byte[])ms.ToArray(); return compressedData; } catch { throw; } } public byte[] Decompress(byte[] bytInput) { MemoryStream ms = new MemoryStream(bytInput,0,bytInput.Length); byte[] bytResult =null; string strResult=String.Empty; byte[] writeData = new byte[4096]; Stream s2 =
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream(ms); try { bytResult=ReadFullStream(s2); s2.Close(); return bytResult; } catch { throw; } } public byte[] ReadFullStream (Stream stream) { byte[] buffer = new byte[32768]; using (MemoryStream ms = new MemoryStream()) { while (true) { int read = stream.Read (buffer, 0, buffer.Length); if (read <= 0) return ms.ToArray(); ms.Write (buffer, 0, read); } } } } }
It is important to understand that you do not need to create a Zip file and use the ZipEntry class to use SharpZipLib. You can simply use the DeflaterInputStream to compress to a byte array, and then do whatever you want with that (save to a file, send over the wire, etc.).

Then, I needed to put the UI elements (Grid, buttons, and DataGrid) into the Page Xaml which look like this:
<UserControl xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  x:Class="NotesSerializer.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="600" Height="400">
    <Grid x:Name="LayoutRoot" Background="White" Width="600" Height="400" ShowGridLines="False">
            <Grid.RowDefinitions>
                <RowDefinition Height="120"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100"></ColumnDefinition>
                <ColumnDefinition Width="100"></ColumnDefinition>
                <ColumnDefinition Width="100"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Button x:Name="btnSave" Click="btnSave_Click" Content="Create" Width="100" Height="25" HorizontalAlignment="Left" VerticalAlignment="Bottom"  Grid.Row="1" Grid.Column="0"/>
        <Button x:Name="btnLoad" Click="btnLoad_Click"  Content="Load" Width="100" Height="25" HorizontalAlignment="Right" VerticalAlignment="Bottom"  Grid.Row="1" Grid.Column="1"/>
        <Button x:Name="btnSaveCompressed" Click="btnSaveCompressed_Click"  Content="Save Comp." Width="100" Height="25" HorizontalAlignment="Center" VerticalAlignment="Bottom"  Grid.Row="1" Grid.Column="2"/>
        <Button x:Name="btnNew" Click="btnNew_Click"  Content="New Note" Width="100" Height="25" HorizontalAlignment="Left" VerticalAlignment="Bottom"  Grid.Row="1" Grid.Column="3"/>
         <my:DataGrid x:Name="Grid1" HorizontalAlignment="Center" VerticalAlignment="Top"  AutoGenerateColumns="True" AlternatingRowBackground="AliceBlue" BorderThickness="2" Width="550" Height="250" Grid.Row="1"  Grid.ColumnSpan="4">
         </my:DataGrid>        
    </Grid>
</UserControl>
Finally, here is the codebehind for the Page with all the code that handles the UI and saving / loading the serialized, compressed List that we are working with:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using System.Windows.Controls;
using ICSharpCode.SharpZipLib;

namespace NotesSerializer
{
    public partial class Page : UserControl
    {
        private List LNotes;
        private Note note;
        private Note note2;
             

        public Page()
        {
            InitializeComponent();
        }

        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            // (this is actually  for the create button)
            note = new Note("Test", "This is a test", Priority.High, Category.Personal, DateTime.Now, false);
           note2 = new Note("Test2", "This is a test2", Priority.Low, Category.Business, DateTime.Now, true);
            LNotes = new List();
            LNotes.Add(note);
            LNotes.Add(note2);
            Grid1.ItemsSource = LNotes;
        }

        private void SaveNotes()
        {
            var w = new Wrapper();
            byte[] b = w.SerializeAndCompress(LNotes);
            using (IsolatedStorageFile isoStore =
                IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (var isoStream =
                    new IsolatedStorageFileStream("notes.dat",
                                                  FileMode.Create, isoStore))
                {
                    isoStream.Write(b, 0, b.Length);
                
                }
             } 
          }
        

        private void LoadSavednotes()
        {
            var notesBytes = new byte[1024];
            byte[] fullnotesBytes = null;
            var ms = new MemoryStream();

            using (IsolatedStorageFile isoStore =
                IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (var isoStream =
                    new IsolatedStorageFileStream("notes.dat",
                                   FileMode.Open,FileAccess.Read, isoStore))
                {
                    using (var reader = new BinaryReader(isoStream))
                    {
                        while (true)
                        {
                            int read = reader.Read(notesBytes, 0, notesBytes.Length);
                            if (read <= 0)
                            {
                                fullnotesBytes = ms.ToArray();
                                break;
                            }
                            ms.Write(notesBytes, 0, read);
                        }
                    }
                }
                byte[] b = ms.ToArray();
                var w = new Wrapper();
                LNotes = (List) w.DecompressAndDeserialize(typeof (List), b);
                Grid1.ItemsSource = LNotes;
            }
        }


        private void btnLoad_Click(object sender, RoutedEventArgs e)
        {
            LoadSavednotes();
        }

        private void btnSaveCompressed_Click(object sender, RoutedEventArgs e)
        {
            SaveNotes();
        }

        private void btnNew_Click(object sender, RoutedEventArgs e)
        {
            Note note = new Note("", "", Priority.Medium, Category.Personal, DateTime.Now, false);
            LNotes.Add(note);
            Grid1.ItemsSource = null;
            Grid1.ItemsSource = LNotes;
        }
    }
}
When you load the app, here's what you should see, right after you hit the "Create" button:

 


"Create" just adds two Note instances to "LNotes", which is a class-level List of type Note, and binds the DataGrid to the collection. This is just so that we can start out with something to work with. I didn't bother to figure out how to make dropdowns out of the Category and Priority enums since this is just a proof of concept. I didn't add a Delete function either. But the general concept is clear: you can build apps that use IsolatedStorage as a client-side data store, and if the data is compressed, you can store one heck of a lot of "stuff".

"New Note" adds a new Note to the List, and rebinds the grid, basically giving you a new Note to fill in.

"Save Comp" serializes and compresses the LNotes List and saves it to an IsolatedStorage file "notes.dat".

And "Load" Loads the notes.dat file bytes, decompresses it, and then deserializes into a new LNotes list and binds the grid. If you restart the app at a later time, and then hit the LOAD button, you'll get back all your work - all saved in highly compressed form on the client, from IsolatedStorage.

You can download the Visual Studio 2008 Silverlight Application here.

View Part I of this series.

View Part II of the series.
By Peter Bromberg   Popularity  (6617 Views)