Silverlight 2 Beta 2: Doing Data Part III


By Peter Bromberg
Printer Friendly Version
  

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 had just been copied and pasted into place - there IS NO XmlSerializer in Beta 2 (yet)! 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!

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.


Biography
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking ,financial and telephony for 20 years. Pete focuses exclusively on the .NET Platform, and his samples at GotDotNet.com have been downloaded over 56,000 times. Peter enjoys producing 3D raytraced digital photo collage with Maya, the beach, and fine wines. You can view Peter's UnBlogIttyUrl, and BlogMetafinder sites.
Please post questions at forums, not via email!

button
 
Article Discussion: Silverlight 2 Beta 2: Doing Data Part III
Peter Bromberg posted at 04-Jul-08 02:58
Original Article

 
IsolatedStorage, Silverlight, and Web Browers
Robbe Morris replied to Peter Bromberg at 04-Jul-08 04:47

Pete,

For those who do not do much with IsolatedStorage or perhaps don't even know what it means, can you provide a little information on gotchas, size maximums, potential security roadblocks that corporations might throw up via pc security configurations, and any browser specific behavior that might be relevant to implementing your strategy in SilverLight.


 
IsolatedStorage
Peter Bromberg replied to Robbe Morris at 16-Aug-08 06:30

In the .NET Framework with isolated storage, data is always isolated by user and by assembly. Credentials such as the origin or the strong name of the assembly determine assembly identity. Data can also be isolated by application domain, using similar credentials.

When using isolated storage for Silverlight apps, applications save data to a unique data compartment that is associated with  either the application, or the local machine. This is the ONLY direct client - side filesystem access that is permitted under the Silverlight browser security sandbox.

The .NET Framework (and the Silverlight subset framework) provides access to Isolated Storage via the IsolatedStorage classes. Think of it as a local client-side data store that is accessible only by the current user on their own machine via a Silverlight application that is running in their browser. The default space allowed is 1 MB, but the API allows you to request the user to increase this at their discretion.


 
maybe Compress could be improved
Wilson Chan replied to Peter Bromberg at 07-Jul-08 06:48

Hi,

    those 4 Compress functions are more or less the same, maybe digest them and make a more reuseful function could be better :)


 
They are convenience overloads.
Peter Bromberg replied to Wilson Chan at 09-Jul-08 03:58
I put them there for convenience depending on the input and output types. Developers do this all the time. This is called "method overloading".